diff --git a/src/mdpo/io.py b/src/mdpo/io.py index 34c2aaf..6ba8f2e 100644 --- a/src/mdpo/io.py +++ b/src/mdpo/io.py @@ -130,3 +130,8 @@ def save_file_checking_file_changed(filepath, content, encoding='utf-8'): with open(filepath, 'w', encoding=encoding) as f: f.write(content) return changed + + +def flatten(xss): + """Flatten a iterable of iterables.""" + return (x for xs in xss for x in xs) diff --git a/src/mdpo/md2po/__init__.py b/src/mdpo/md2po/__init__.py index 2eba65d..bd24e02 100644 --- a/src/mdpo/md2po/__init__.py +++ b/src/mdpo/md2po/__init__.py @@ -14,6 +14,7 @@ from mdpo.event import add_debug_events, parse_events_kwarg, raise_skip_event from mdpo.io import ( filter_paths, + flatten, save_file_checking_file_changed, to_files_or_content, ) @@ -74,7 +75,6 @@ def transform_foo(self, block, text): 'include_codeblocks', 'metadata', 'events', - 'obsoletes', 'location', '_current_top_level_block_number', @@ -142,11 +142,8 @@ def transform_foo(self, block, text): def __init__(self, files_or_content, **kwargs): is_glob, files_or_content = to_files_or_content(files_or_content) if is_glob: - filepaths = [] - for globpath in files_or_content: - filepaths.extend(glob.glob(globpath)) self.filepaths = filter_paths( - filepaths, + flatten(glob.glob(globpath) for globpath in files_or_content), ignore_paths=kwargs.get('ignore', []), ) else: @@ -183,10 +180,6 @@ def __init__(self, files_or_content, **kwargs): if kwargs.get('debug'): add_debug_events('md2po', self.events) - #: bool: If there are entries in the PO file that - #: are obsolete and not found in the current extraction. - self.obsoletes = False - #: str: The msgid being currently built for the next #: message entry. Keep in mind that, if you are executing #: an event that will be followed by an span one @@ -992,10 +985,6 @@ def extract( wrapwidth=parse_wrapwidth_argument(wrapwidth), **pofile_kwargs, ) - for entry in self.pofile: - if entry.obsolete: - self.obsoletes = True - break parser = md4c.GenericParser( 0, @@ -1044,12 +1033,10 @@ def _parse(content): self.found_entries, ) elif self.mark_not_found_as_obsolete: - obsoletes = mark_not_found_entries_as_obsoletes( + mark_not_found_entries_as_obsoletes( self.pofile, self.found_entries, ) - if not self.obsoletes: - self.obsoletes = obsoletes if self.metadata: self.pofile.metadata.update(self.metadata) diff --git a/src/mdpo/md2po/__main__.py b/src/mdpo/md2po/__main__.py index d6bf3c1..aa29400 100755 --- a/src/mdpo/md2po/__main__.py +++ b/src/mdpo/md2po/__main__.py @@ -31,6 +31,7 @@ from mdpo.io import environ from mdpo.md2po import Md2Po from mdpo.md4c import DEFAULT_MD4C_GENERIC_PARSER_EXTENSIONS +from mdpo.po import check_obsolete_entries_in_filepaths DESCRIPTION = ( @@ -232,17 +233,23 @@ def run(args=frozenset()): if opts.check_saved_files_changed and md2po._saved_files_changed: exitcode = 2 - if opts.no_obsolete and md2po.obsoletes: - if not opts.quiet: - sys.stderr.write( - ( - f"Obsolete messages found at {opts.po_filepath}" - " and passed '--no-obsolete'\n" - ), - ) - exitcode = 3 - - if opts.no_empty_msgstr: + if opts.no_obsolete: + locations = list(check_obsolete_entries_in_filepaths( + (opts.po_filepath,), + )) + if locations: + if len(locations) > 2: # noqa PLR2004 + sys.stderr.write( + f'Found {len(locations)} obsolete entries:\n', + ) + for location in locations: + sys.stderr.write(f'{location}\n') + else: + for location in locations: + sys.stderr.write( + f'Found obsolete entry at {location}\n') + exitcode = 3 + elif opts.no_empty_msgstr: for entry in pofile: if not entry.msgstr: if not opts.quiet: diff --git a/src/mdpo/md2po2md/__init__.py b/src/mdpo/md2po2md/__init__.py index 8f543d7..a3d3be2 100644 --- a/src/mdpo/md2po2md/__init__.py +++ b/src/mdpo/md2po2md/__init__.py @@ -5,6 +5,7 @@ from mdpo.md2po import Md2Po from mdpo.md4c import DEFAULT_MD4C_GENERIC_PARSER_EXTENSIONS +from mdpo.po import check_obsolete_entries_in_filepaths from mdpo.po2md import Po2Md @@ -24,6 +25,7 @@ def markdown_to_pofile_to_markdown( md2po_kwargs=None, po2md_kwargs=None, _check_saved_files_changed=False, + no_obsolete=False, ): """Translate a set of Markdown files using PO files. @@ -70,6 +72,7 @@ def markdown_to_pofile_to_markdown( ``markdown_to_pofile`` function. po2md_kwargs (dict): Additional optional arguments passed to ``pofile_to_markdown`` function. + no_obsolete (bool): If ``True``, check for obsolete entries in PO files. """ if '{lang}' not in output_paths_schema: raise ValueError( @@ -97,7 +100,7 @@ def markdown_to_pofile_to_markdown( ) _saved_files_changed = None if not _check_saved_files_changed else False - obsoletes = False + obsoletes = [] empty = False for filepath in input_paths_glob_: @@ -156,8 +159,6 @@ def markdown_to_pofile_to_markdown( if _check_saved_files_changed and _saved_files_changed is False: _saved_files_changed = md2po._saved_files_changed - if not obsoletes: - obsoletes = md2po.obsoletes if not empty: for entry in md2po.pofile: if not entry.msgstr: @@ -182,15 +183,10 @@ def markdown_to_pofile_to_markdown( if _check_saved_files_changed and _saved_files_changed is False: _saved_files_changed = po2md._saved_files_changed - if not obsoletes: - for pofile in po2md.pofiles: - for entry in pofile: - if entry.obsolete: - obsoletes = True - break - if obsoletes: - break - + if no_obsolete: + obsoletes.extend(check_obsolete_entries_in_filepaths( + [po_filepath], + )) if not empty: for pofile in po2md.pofiles: for entry in pofile: diff --git a/src/mdpo/md2po2md/__main__.py b/src/mdpo/md2po2md/__main__.py index ea7ea49..c466777 100755 --- a/src/mdpo/md2po2md/__main__.py +++ b/src/mdpo/md2po2md/__main__.py @@ -140,6 +140,7 @@ def run(args=frozenset()): 'md_encoding': opts.md_encoding, 'include_codeblocks': opts.include_codeblocks, '_check_saved_files_changed': opts.check_saved_files_changed, + 'no_obsolete': opts.no_obsolete, } ( @@ -155,16 +156,20 @@ def run(args=frozenset()): if opts.check_saved_files_changed and _saved_files_changed: exitcode = 2 - if opts.no_obsolete and obsoletes: - exitcode = 3 - - if not opts.quiet: + if obsoletes: + if len(obsoletes) > 2: # noqa PLR2004 sys.stderr.write( - "Obsolete messages found at PO files and" - " passed '--no-obsolete'\n", + f'Found {len(obsoletes)} obsolete entries:\n', ) - - if opts.no_empty_msgstr and empty: + for location in obsoletes: + sys.stderr.write(f'{location}\n') + else: + for location in obsoletes: + sys.stderr.write( + f'Found obsolete entry at {location}\n', + ) + exitcode = 3 + elif opts.no_empty_msgstr and empty: exitcode = 4 if not opts.quiet: diff --git a/src/mdpo/po.py b/src/mdpo/po.py index dde3408..6c3f2d3 100644 --- a/src/mdpo/po.py +++ b/src/mdpo/po.py @@ -168,3 +168,45 @@ def paths_or_globs_to_unique_pofiles(pofiles_globs, ignore, po_encoding=None): _po_filepaths.append(po_filepath) return pofiles + + +def check_obsolete_entries_in_filepaths(filenames): + """Warns about all obsolete entries found in a set of PO files. + + Args: + filenames (list): Set of file names to check. + + Returns: + list(str): error messages produced. + """ + for filename in filenames: + with open(filename, 'rb') as f: + content_lines = f.readlines() + + yield from parse_obsoletes_from_content_lines( + content_lines, + location_prefix=f'{filename}:', + ) + + +def parse_obsoletes_from_content_lines( + content_lines, + location_prefix='line ', +): + """Warns about all obsolete entries found in a set of PO files. + + Args: + content_lines (list): Set of content lines to check. + location_prefix (str): Prefix to use in the location message. + + Returns: + list(str): error locations found. + """ + inside_obsolete_message = False + for i, line in enumerate(content_lines): + if not inside_obsolete_message and line[0:3] == b'#~ ': + inside_obsolete_message = True + + yield f'{location_prefix}{i + 1}' + elif inside_obsolete_message and line[0:3] != b'#~ ': + inside_obsolete_message = False diff --git a/src/mdpo/po2md/__main__.py b/src/mdpo/po2md/__main__.py index c4dee35..2309463 100755 --- a/src/mdpo/po2md/__main__.py +++ b/src/mdpo/po2md/__main__.py @@ -24,6 +24,10 @@ parse_event_argument, ) from mdpo.io import environ +from mdpo.po import ( + check_obsolete_entries_in_filepaths, + paths_or_globs_to_unique_pofiles, +) from mdpo.po2md import Po2Md @@ -136,15 +140,28 @@ def run(args=frozenset()): if opts.check_saved_files_changed and po2md._saved_files_changed: return (output, 2) - if opts.no_obsolete and get_obsoletes(po2md.pofiles): - if not opts.quiet: - sys.stderr.write( - - "Obsolete messages found at PO files and passed" - " '--no-obsolete'\n", - - ) - return (output, 3) + if opts.no_obsolete: + pofiles = paths_or_globs_to_unique_pofiles( + opts.pofiles, + opts.ignore or [], + po_encoding=opts.po_encoding, + ) + locations = list(check_obsolete_entries_in_filepaths( + pofiles, + )) + if locations: + if len(locations) > 2: # noqa PLR2004 + sys.stderr.write( + f'Found {len(locations)} obsolete entries:\n', + ) + for location in locations: + sys.stderr.write(f'{location}\n') + else: + for location in locations: + sys.stderr.write( + f'Found obsolete entry at {location}\n', + ) + return (output, 3) if opts.no_empty_msgstr: for pofile in po2md.pofiles: diff --git a/tests/test_unit/test_md2po/test_md2po_cli.py b/tests/test_unit/test_md2po/test_md2po_cli.py index 7aa68dd..3578b1b 100644 --- a/tests/test_unit/test_md2po/test_md2po_cli.py +++ b/tests/test_unit/test_md2po/test_md2po_cli.py @@ -744,14 +744,14 @@ def test_no_obsolete(capsys, arg, tmp_file): with tmp_file(po_input, '.po') as filename: pofile, exitcode = run([arg, '-p', filename, '--no-location', 'Bye']) - stdout, stderr = capsys.readouterr() + stdout, stderr = capsys.readouterr() - assert exitcode == 3 - assert f'{pofile}\n' == expected_output - assert stdout == expected_output - assert stderr == ( - f"Obsolete messages found at {filename} and passed '--no-obsolete'\n" - ) + assert exitcode == 3 + assert f'{pofile}\n' == expected_output + assert stdout == expected_output + assert stderr == ( + f'Found obsolete entry at {filename}:5\n' + ) po_input = '''# msgid "" @@ -776,6 +776,55 @@ def test_no_obsolete(capsys, arg, tmp_file): assert stdout == expected_output +@pytest.mark.parametrize('arg', ('--no-obsolete',)) +def test_no_obsolete_multiple(capsys, arg, tmp_file): + po_input = '''# +msgid "" +msgstr "" + +#~ msgid "Foo" +#~ msgstr "Foo lang" + +#~ msgid "Bar" +#~ msgstr "Bar lang" + +#~ msgid "Baz" +#~ msgstr "Baz lang" +''' + + expected_output = '''# +msgid "" +msgstr "" + +msgid "Bye" +msgstr "" + +#~ msgid "Foo" +#~ msgstr "Foo lang" + +#~ msgid "Bar" +#~ msgstr "Bar lang" + +#~ msgid "Baz" +#~ msgstr "Baz lang" + +''' + + with tmp_file(po_input, '.po') as filename: + pofile, exitcode = run([arg, '-p', filename, '--no-location', 'Bye']) + stdout, stderr = capsys.readouterr() + + assert exitcode == 3 + assert f'{pofile}\n' == expected_output + assert stdout == expected_output + assert stderr == ( + 'Found 3 obsolete entries:\n' + f'{filename}:5\n' + f'{filename}:8\n' + f'{filename}:11\n' + ) + + @pytest.mark.parametrize('arg', ('--no-empty-msgstr',)) def test_no_empty_mgstr(capsys, arg, tmp_file): po_input = '''#