From af1fa5e7da21c57a4037e67f93493af4e78d454a Mon Sep 17 00:00:00 2001 From: Pavel Kulikov Date: Sun, 26 Mar 2017 22:29:05 +0300 Subject: [PATCH] Fixed #27978 -- Allowed loaddata to read data from stdin. Thanks Squareweave for the django-loaddata-stdin project from which this is adapted. --- AUTHORS | 1 + django/core/management/commands/loaddata.py | 17 ++++++++++++ docs/ref/django-admin.txt | 29 +++++++++++++++++++++ docs/releases/2.0.txt | 2 ++ tests/fixtures/tests.py | 29 +++++++++++++++++++++ 5 files changed, 78 insertions(+) diff --git a/AUTHORS b/AUTHORS index 30a445b91c..fbf66bacbf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -617,6 +617,7 @@ answer newbie questions, and generally made Django that much better: Paulo Poiati Paulo Scardine Paul Smith + Pavel Kulikov pavithran s Pavlo Kapyshin permonik@mesias.brnonet.cz diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 395d459378..2c19350cc4 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -2,6 +2,7 @@ import functools import glob import gzip import os +import sys import warnings import zipfile from itertools import product @@ -25,6 +26,8 @@ try: except ImportError: has_bz2 = False +READ_STDIN = '-' + class Command(BaseCommand): help = 'Installs the named fixture(s) in the database.' @@ -52,6 +55,10 @@ class Command(BaseCommand): '-e', '--exclude', dest='exclude', action='append', default=[], 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): self.ignore = options['ignore'] @@ -59,6 +66,7 @@ class Command(BaseCommand): self.app_label = options['app_label'] self.verbosity = options['verbosity'] self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(options['exclude']) + self.format = options['format'] with transaction.atomic(using=self.using): self.loaddata(fixture_labels) @@ -85,6 +93,7 @@ class Command(BaseCommand): None: (open, 'rb'), 'gz': (gzip.GzipFile, 'rb'), 'zip': (SingleZipReader, 'r'), + 'stdin': (lambda *args: sys.stdin, None), } if has_bz2: self.compression_formats['bz2'] = (bz2.BZ2File, 'r') @@ -201,6 +210,9 @@ class Command(BaseCommand): @functools.lru_cache(maxsize=None) def find_fixtures(self, fixture_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) databases = [self.using, None] 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. """ + 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) if len(parts) > 1 and parts[-1] in self.compression_formats: diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 2de0ab6a8b..68fb762623 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -416,6 +416,14 @@ originally generated. 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 ` (e.g., +``json`` or ``xml``) for fixtures :ref:`read from stdin +`. + .. django-admin-option:: --exclude EXCLUDE, -e EXCLUDE .. 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 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 ` option +is required to specify the :ref:`serialization format ` +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`` ---------------- diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 186f64319d..de5b38a098 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -185,6 +185,8 @@ Management Commands * The new :option:`makemessages --add-location` option controls the comment format in PO files. +* :djadmin:`loaddata` can now :ref:`read from stdin `. + Migrations ~~~~~~~~~~ diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index fe047e9838..fb9ba91d3d 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -680,6 +680,35 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): with self.assertRaisesMessage(management.CommandError, msg): 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(), [ + '', + '', + ]) + + 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(), [ + '', + '', + '', + ]) + class NonexistentFixtureTests(TestCase): """