From 0a14b7884700276ce9289ad102cfa6bcc81eb0e2 Mon Sep 17 00:00:00 2001 From: Krzysztof Jagiello Date: Tue, 25 Jan 2022 21:59:17 +0100 Subject: [PATCH] Provide a way of checking if the catalogs are up-to-date --- babel/messages/catalog.py | 23 ++++++++++++++ babel/messages/frontend.py | 39 +++++++++++++++++++++-- tests/messages/test_frontend.py | 55 ++++++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index f9e377dc0..228b10b71 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -139,6 +139,13 @@ def __eq__(self, other): def __ne__(self, other): return self.__cmp__(other) != 0 + def is_identical(self, other): + """Checks whether messages are identical, taking into account all + properties. + """ + assert isinstance(other, Message) + return self.__dict__ == other.__dict__ + def clone(self): return Message(*map(copy, (self.id, self.string, self.locations, self.flags, self.auto_comments, @@ -837,3 +844,19 @@ def _key_for(self, id, context=None): if context is not None: key = (key, context) return key + + def is_identical(self, other): + """Checks if catalogs are identical, taking into account messages and + headers. + """ + assert isinstance(other, Catalog) + for key in self._messages.keys() | other._messages.keys(): + message_1 = self.get(key) + message_2 = other.get(key) + if ( + message_1 is None + or message_2 is None + or not message_1.is_identical(message_2) + ): + return False + return dict(self.mime_headers) == dict(other.mime_headers) diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index c71bc0d79..615952ac8 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -41,14 +41,15 @@ distutils_log = log # "distutils.log → (no replacement yet)" try: - from setuptools.errors import OptionError, SetupError + from setuptools.errors import OptionError, SetupError, BaseError except ImportError: # Error aliases only added in setuptools 59 (2021-11). - OptionError = SetupError = Exception + OptionError = SetupError = BaseError = Exception except ImportError: from distutils import log as distutils_log from distutils.cmd import Command as _Command - from distutils.errors import OptionError as OptionError, DistutilsSetupError as SetupError + from distutils.errors import OptionError as OptionError, DistutilsSetupError as SetupError, DistutilsError as BaseError + def listify_value(arg, split=None): @@ -714,10 +715,15 @@ class update_catalog(Command): 'update target header comment'), ('previous', None, 'keep previous msgids of translated messages'), + ('check=', None, + 'don\'t update the catalog, just return the status. Return code 0 ' + 'means nothing would change. Return code 1 means that the catalog ' + 'would be updated'), ] boolean_options = [ 'omit-header', 'no-wrap', 'ignore-obsolete', 'init-missing', 'no-fuzzy-matching', 'previous', 'update-header-comment', + 'check', ] def initialize_options(self): @@ -734,6 +740,7 @@ def initialize_options(self): self.no_fuzzy_matching = False self.update_header_comment = False self.previous = False + self.check = False def finalize_options(self): if not self.input_file: @@ -767,6 +774,7 @@ def finalize_options(self): self.previous = False def run(self): + check_status = {} po_files = [] if not self.output_file: if self.locale: @@ -796,6 +804,9 @@ def run(self): for locale, filename in po_files: if self.init_missing and not os.path.exists(filename): + if self.check: + check_status[filename] = False + continue self.log.info( 'creating catalog %s based on %s', filename, self.input_file ) @@ -834,6 +845,16 @@ def run(self): os.remove(tmpname) raise + if self.check: + with open(filename, "rb") as origfile: + original_catalog = read_po(origfile) + with open(tmpname, "rb") as newfile: + updated_catalog = read_po(newfile) + updated_catalog.revision_date = original_catalog.revision_date + check_status[filename] = updated_catalog.is_identical(original_catalog) + os.remove(tmpname) + continue + try: os.rename(tmpname, filename) except OSError: @@ -846,6 +867,18 @@ def run(self): shutil.copy(tmpname, filename) os.remove(tmpname) + if self.check: + for filename, up_to_date in check_status.items(): + if up_to_date: + self.log.info('Catalog %s is up to date.', filename) + else: + self.log.warning('Catalog %s is out of date.', filename) + if not all(check_status.values()): + raise BaseError("Some catalogs are out of date.") + else: + self.log.info("All the catalogs are up-to-date.") + return + class CommandLineInterface(object): """Command-line interface. diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index f8a58dd21..e480b9ce0 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -27,7 +27,7 @@ from babel import __version__ as VERSION from babel.dates import format_datetime from babel.messages import frontend, Catalog -from babel.messages.frontend import CommandLineInterface, extract_messages, update_catalog, OptionError +from babel.messages.frontend import CommandLineInterface, extract_messages, update_catalog, OptionError, BaseError from babel.util import LOCALTZ from babel.messages.pofile import read_po, write_po @@ -1203,6 +1203,59 @@ def test_update(self): catalog = read_po(infp) assert len(catalog) == 4 # Catalog was updated + def test_check(self): + template = Catalog() + template.add("1") + template.add("2") + template.add("3") + tmpl_file = os.path.join(i18n_dir, 'temp-template.pot') + with open(tmpl_file, "wb") as outfp: + write_po(outfp, template) + po_file = os.path.join(i18n_dir, 'temp1.po') + + # Update the catalog file + self.cli.run(sys.argv + ['update', + '-l', 'fi_FI', + '-o', po_file, + '-i', tmpl_file]) + + # Run a check without introducing any changes to the template + self.cli.run(sys.argv + ['update', + '--check', + '-l', 'fi_FI', + '-o', po_file, + '-i', tmpl_file]) + + # Add a new entry and expect the check to fail + template.add("4") + with open(tmpl_file, "wb") as outfp: + write_po(outfp, template) + + with self.assertRaises(BaseError): + self.cli.run(sys.argv + ['update', + '--check', + '-l', 'fi_FI', + '-o', po_file, + '-i', tmpl_file]) + + # Write the latest changes to the po-file + self.cli.run(sys.argv + ['update', + '-l', 'fi_FI', + '-o', po_file, + '-i', tmpl_file]) + + # Update an entry and expect the check to fail + template.add("4", locations=[("foo.py", 1)]) + with open(tmpl_file, "wb") as outfp: + write_po(outfp, template) + + with self.assertRaises(BaseError): + self.cli.run(sys.argv + ['update', + '--check', + '-l', 'fi_FI', + '-o', po_file, + '-i', tmpl_file]) + def test_update_init_missing(self): template = Catalog() template.add("1")