From cc31a71ce7130ec503f627388918d8d7f095fc59 Mon Sep 17 00:00:00 2001 From: Shadi Naif Date: Fri, 18 Aug 2023 11:48:57 +0300 Subject: [PATCH] feat: add new options merge-js and no-partial no-partial: no partial files are generated, because they replaced the original django.po and djangojs.po merge-js: no djangojs*.po is generated, because it has been merged into django*.po Refs: FC-0012 OEP-58 --- i18n/extract.py | 49 +++++++++++++++++++++++-- tests/test_extract.py | 85 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 118 insertions(+), 16 deletions(-) diff --git a/i18n/extract.py b/i18n/extract.py index 89203b5..cbd9aae 100644 --- a/i18n/extract.py +++ b/i18n/extract.py @@ -26,7 +26,7 @@ from path import Path from i18n import Runner -from i18n.execute import execute +from i18n.execute import remove_file, execute from i18n.segment import segment_pofiles @@ -65,6 +65,22 @@ def add_args(self): Adds arguments """ self.parser.description = __doc__ + self.parser.add_argument( + '--merge-js', + action='store_true', + help='Merge djangojs.po with django.po using msgcat' + ) + self.parser.add_argument( + '--no-partial', + action='store_true', + help=( + 'Renames (django-partial.po) and (djangojs-partial.po) to (django.po) and (djangojs.po) respectively. ' + 'This will also remove (django-saved.po) and (djangojs-saved.po) that are used to restore original ' + '(django.po) and (djangojs.po) after extract is completed. ' + 'This is helpful when this `extract` command output is needed to be used immediately ' + 'with no segment/merge workflow in small repositories.' + ) + ) def rename_source_file(self, src, dst): """ @@ -157,9 +173,19 @@ def run(self, args): for filename in files_to_clean: clean_pofile(self.source_msgs_dir.joinpath(filename)) - # Restore the saved .po files. - self.rename_source_file(DJANGO_SAVED_PO, DJANGO_PO) - self.rename_source_file(DJANGOJS_SAVED_PO, DJANGOJS_PO) + if args.merge_js: + self.merge_js(stderr) + + if args.no_partial: + # Overwrite django.po and djangojs.po from django-partial.po and djangojs-partial.po + self.rename_source_file(DJANGO_PARTIAL_PO, DJANGO_PO) + self.rename_source_file(DJANGOJS_PARTIAL_PO, DJANGOJS_PO) + remove_file(self.source_msgs_dir.joinpath(DJANGO_SAVED_PO)) + remove_file(self.source_msgs_dir.joinpath(DJANGOJS_SAVED_PO)) + else: + # Restore the saved .po files. + self.rename_source_file(DJANGO_SAVED_PO, DJANGO_PO) + self.rename_source_file(DJANGOJS_SAVED_PO, DJANGOJS_PO) def babel_extract(self, stderr, verbosity): """ @@ -197,6 +223,21 @@ def babel_extract(self, stderr, verbosity): execute(babel_underscore_cmd, working_directory=configuration.root_dir, stderr=stderr) + def merge_js(self, stderr): + """ + Merge djangojs.po with django.po using msgcat + """ + # Some projects don't have any javascript, so there is no djangojs-partial.po + if not file_exists(self.source_msgs_dir.joinpath()): + return + + execute( + 'msgcat django-partial.po djangojs-partial.po -o django-partial.po', + working_directory=self.source_msgs_dir, + stderr=stderr, + ) + remove_file(self.source_msgs_dir.joinpath(DJANGOJS_PARTIAL_PO)) + def clean_pofile(path_name): """ diff --git a/tests/test_extract.py b/tests/test_extract.py index c057f60..1718522 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -1,7 +1,9 @@ import os from datetime import datetime, timedelta from functools import wraps +import itertools +import ddt import polib from i18n import extract, config from path import Path @@ -9,29 +11,42 @@ from . import I18nToolTestCase, MOCK_DJANGO_APP_DIR -def perform_extract(): +def perform_extract_with_options(): """ Decorator for test methods in TestExtract class. + + It wraps the test method in a function that calls (extract.main) with various options using (ddt.data) + + Sets the following attributes: + - ddt_flag_merge_js: True if the test method should be run with merge-js=True + - ddt_flag_no_partial: True if the test method should be run with no-partial=True """ def decorator(test_method): """ The decorator itself """ @wraps(test_method) - def wrapped(self): + @ddt.data(*itertools.product([True, False], repeat=2)) # all combinations of flags + @ddt.unpack + def wrapped(self, flag_merge_js, flag_no_partial): """ The wrapper function """ + self.ddt_flag_merge_js = flag_merge_js + self.ddt_flag_no_partial = flag_no_partial extract.main( verbosity=0, config=self.configuration._filename, root_dir=MOCK_DJANGO_APP_DIR, + merge_js=flag_merge_js, + no_partial=flag_no_partial, ) test_method(self) return wrapped return decorator +@ddt.ddt class TestExtract(I18nToolTestCase): """ Tests functionality of i18n/extract.py @@ -52,19 +67,24 @@ def setUp(self): ) self.configuration = config.Configuration(root_dir=MOCK_DJANGO_APP_DIR) + # These will be set by extract_options decorator. Also get_files will fail if they are None (to remind us + # to use the decorator in all tests methods) + self.ddt_flag_merge_js = None + self.ddt_flag_no_partial = None + @property def django_po(self): """ - Returns the name of the generated django file + Returns file or partial file name according to the no-partial flag """ - return extract.DJANGO_PARTIAL_PO + return extract.DJANGO_PO if self.ddt_flag_no_partial else extract.DJANGO_PARTIAL_PO @property def djangojs_po(self): """ - Returns the name of the generated djangojs file + Returns jsfile or partial jsfile name according to the no-partial flag """ - return extract.DJANGOJS_PARTIAL_PO + return extract.DJANGOJS_PO if self.ddt_flag_no_partial else extract.DJANGOJS_PARTIAL_PO def get_files(self): """ @@ -72,7 +92,15 @@ def get_files(self): Returns the fully expanded filenames for all extracted files Fails assertion if one of the files doesn't exist. """ - generated_files = (extract.MAKO_PO, self.django_po, self.djangojs_po,) + assert self.ddt_flag_merge_js is not None, "Use perform_extract decorator" + assert self.ddt_flag_no_partial is not None, "Use perform_extract decorator" + + # Depending on how the merge-js and no-partial options are set, we may have generated different files + # no-partial: no partial files are generated, because they replaced the original django.po and djangojs.po + # merge-js: no djangojs*.po is generated, because it has been merged into django*.po + generated_files = ('mako.po', self.django_po,) + if not self.ddt_flag_merge_js: + generated_files += (self.djangojs_po,) for filename in generated_files: path = Path.joinpath(self.configuration.source_messages_dir, filename) @@ -81,7 +109,7 @@ def get_files(self): yield path - @perform_extract() + @perform_extract_with_options() def test_files(self): """ Asserts that each auto-generated file has been modified since 'extract' was launched. @@ -91,7 +119,7 @@ def test_files(self): self.assertTrue(datetime.fromtimestamp(os.path.getmtime(path)) > self.start_time, msg='File not recently modified: %s' % path) - @perform_extract() + @perform_extract_with_options() def test_is_keystring(self): """ Verifies is_keystring predicate @@ -103,7 +131,7 @@ def test_is_keystring(self): self.assertTrue(extract.is_key_string(entry1.msgid)) self.assertFalse(extract.is_key_string(entry2.msgid)) - @perform_extract() + @perform_extract_with_options() def test_headers(self): """ Verify all headers have been modified @@ -116,7 +144,7 @@ def test_headers(self): msg='Missing header in %s:\n"%s"' % (path, header) ) - @perform_extract() + @perform_extract_with_options() def test_metadata(self): """ Verify all metadata has been modified @@ -128,7 +156,7 @@ def test_metadata(self): expected = 'openedx-translation@googlegroups.com' self.assertEquals(expected, value) - @perform_extract() + @perform_extract_with_options() def test_metadata_no_create_date(self): """ Verify `POT-Creation-Date` metadata has been removed @@ -137,3 +165,36 @@ def test_metadata_no_create_date(self): po = polib.pofile(path) metadata = po.metadata self.assertIsNone(metadata.get('POT-Creation-Date')) + + @perform_extract_with_options() + def test_merge_js(self): + """ + Verify that djangojs*.po is generated only if merge-js is False + """ + assert self.ddt_flag_merge_js != Path.exists( + Path.joinpath(self.configuration.source_messages_dir, self.djangojs_po,) + ) + + @perform_extract_with_options() + def test_no_partial_guard_to_verify_names_in_tests(self): + """ + Verify that (django_po) and (djangojs_po) properties return the correct file names + according to no-partial flag + """ + assert self.ddt_flag_no_partial != ('-partial' in self.django_po) + assert self.ddt_flag_no_partial != ('-partial' in self.djangojs_po) + + @perform_extract_with_options() + def test_no_partial(self): + """ + Verify that partial files are not generated if no-partial is True + """ + # We can't use (django_po) and (djangojs_po) properties here because we need to always check + # for (partial) files + assert self.ddt_flag_no_partial != Path.exists( + Path.joinpath(self.configuration.source_messages_dir, extract.DJANGO_PARTIAL_PO,) + ) + if not self.ddt_flag_merge_js: + assert self.ddt_flag_no_partial != Path.exists( + Path.joinpath(self.configuration.source_messages_dir, extract.DJANGOJS_PARTIAL_PO,) + )