mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	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:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							c930c241f8
						
					
				
				
					commit
					af1fa5e7da
				
			
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							| @@ -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 | |||||||
| 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: | ||||||
|   | |||||||
| @@ -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`` | ||||||
| ---------------- | ---------------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
| ~~~~~~~~~~ | ~~~~~~~~~~ | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								tests/fixtures/tests.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								tests/fixtures/tests.py
									
									
									
									
										vendored
									
									
								
							| @@ -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): | ||||||
|     """ |     """ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user