Skip to content

Commit ad322bd

Browse files
committed
Refactor ConcatenateCatalog: stdout output, mutual exclusivity, better help texts
1 parent 6dd98e6 commit ad322bd

2 files changed

Lines changed: 73 additions & 45 deletions

File tree

babel/messages/frontend.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import sys
2222
import tempfile
2323
import warnings
24-
from collections import defaultdict
24+
from collections import Counter, defaultdict
2525
from configparser import RawConfigParser
2626
from io import StringIO
2727
from typing import Any, BinaryIO, Iterable, Literal
@@ -892,19 +892,20 @@ class ConcatenateCatalog(CommandMixin):
892892
description = 'concatenates the specified PO files into single one'
893893
user_options = [
894894
('input-files', None, 'input files'),
895-
('output-file=', 'o', 'write output to specified file'),
896-
('less-than=', '<', 'print messages with less than this many'
897-
'definitions, defaults to infinite if not set '),
895+
('output-file=', 'o', 'write output to specified file, the results are written '
896+
'to standard output if no output file is specified or if it is \'-\''),
897+
('less-than=', '<', 'print messages with less than this many '
898+
'definitions, defaults to infinite if not set'),
898899
('more-than=', '>', 'print messages with more than this many '
899900
'definitions, defaults to 0 if not set'),
900901
('unique', 'u', 'shorthand for --less-than=2, requests '
901902
'that only unique messages be printed'),
902903
('use-first', None, 'use first available translation for each '
903904
'message, don\'t merge several translations'),
904-
('no-location', None, 'do not write \'#: filename:line\' lines'),
905-
('width=', 'w', 'set output page width'),
905+
('no-location', None, 'do not include location comments with filename and line number'),
906+
('width=', 'w', 'set output line width (default 76)'),
906907
('no-wrap', None, 'do not break long message lines, longer than '
907-
'the output page width, into several lines'),
908+
'the output line width, into several lines'),
908909
('sort-output', 's', 'generate sorted output'),
909910
('sort-by-file', 'F', 'sort output by file location'),
910911
]
@@ -937,8 +938,6 @@ def initialize_options(self):
937938
def finalize_options(self):
938939
if not self.input_files:
939940
raise OptionError('you must specify the input files')
940-
if not self.output_file:
941-
raise OptionError('you must specify the output file')
942941

943942
if self.no_wrap and self.width:
944943
raise OptionError("'--no-wrap' and '--width' are mutually exclusive")
@@ -953,31 +952,34 @@ def finalize_options(self):
953952
self.more_than = int(self.more_than)
954953
if self.less_than is not None:
955954
self.less_than = int(self.less_than)
955+
956956
if self.unique:
957+
if self.less_than is not None or self.more_than:
958+
raise OptionError("'--unique' is mutually exclusive with '--less-than' and '--more-than'")
957959
self.less_than = 2
958960

959-
def _prepare(self):
961+
def _collect_message_info(self):
960962
templates: list[tuple[str, Catalog]] = []
961-
message_info = {}
963+
message_counts: Counter = Counter()
964+
message_strings: dict[object, set] = defaultdict(set)
962965

963966
for filename in self.input_files:
964967
with open(filename, 'r') as pofile:
965968
template = read_po(pofile)
966969
for message in template:
967-
if message.id not in message_info:
968-
message_info[message.id] = {
969-
'count': 0,
970-
'strings': set(),
971-
}
972-
message_info[message.id]['count'] += 1
973-
message_info[message.id]['strings'].add(message.string if isinstance(message.string, str) else tuple(message.string))
974-
templates.append((filename, template, ))
970+
if not message.id:
971+
continue
972+
message_counts[message.id] += 1
973+
message_strings[message.id].add(
974+
message.string if isinstance(message.string, str) else tuple(message.string)
975+
)
976+
templates.append((filename, template))
975977

976-
return templates, message_info
978+
return templates, message_counts, message_strings
977979

978980
def run(self):
979981
catalog = Catalog(fuzzy=False)
980-
templates, message_info = self._prepare()
982+
templates, message_counts, message_strings = self._collect_message_info()
981983

982984
for path, template in templates:
983985
if catalog.locale is None:
@@ -987,12 +989,11 @@ def run(self):
987989
if not message.id:
988990
continue
989991

990-
count = message_info[message.id]['count']
991-
diff_string_count = len(message_info[message.id]['strings'])
992+
count = message_counts[message.id]
992993
if count <= self.more_than or (self.less_than is not None and count >= self.less_than):
993994
continue
994995

995-
if count > 1 and not self.use_first and diff_string_count > 1:
996+
if count > 1 and not self.use_first and len(message_strings[message.id]) > 1:
996997
filename = os.path.basename(path)
997998
catalog.add_conflict(message, filename, template.project, template.version)
998999
message.flags |= {'fuzzy'}
@@ -1001,15 +1002,26 @@ def run(self):
10011002

10021003
catalog.fuzzy = any(message.fuzzy for message in catalog)
10031004

1004-
with open(self.output_file, 'wb') as outfile:
1005+
output_file = self.output_file
1006+
if not output_file or output_file == '-':
10051007
write_po(
1006-
outfile,
1008+
sys.stdout.buffer,
10071009
catalog,
10081010
width=self.width,
10091011
sort_by_file=self.sort_by_file,
10101012
sort_output=self.sort_output,
10111013
no_location=self.no_location,
10121014
)
1015+
else:
1016+
with open(output_file, 'wb') as outfile:
1017+
write_po(
1018+
outfile,
1019+
catalog,
1020+
width=self.width,
1021+
sort_by_file=self.sort_by_file,
1022+
sort_output=self.sort_output,
1023+
no_location=self.no_location,
1024+
)
10131025

10141026

10151027
class MergeCatalog(CommandMixin):

tests/messages/frontend/test_concat_merge.py

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,32 +73,47 @@ def teardown_method(self):
7373

7474
def _get_expected(self, messages, fuzzy=False):
7575
date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
76-
return fr"""# Translations template for PROJECT.
77-
# Copyright (C) 1994 ORGANIZATION
78-
# This file is distributed under the same license as the PROJECT project.
79-
# FIRST AUTHOR <EMAIL@ADDRESS>, 1994.
80-
#{'\n#, fuzzy' if fuzzy else ''}
81-
msgid ""
82-
msgstr ""
83-
"Project-Id-Version: PROJECT VERSION\n"
84-
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
85-
"POT-Creation-Date: {date}\n"
86-
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
87-
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
88-
"Language-Team: LANGUAGE <LL@li.org>\n"
89-
"MIME-Version: 1.0\n"
90-
"Content-Type: text/plain; charset=utf-8\n"
91-
"Content-Transfer-Encoding: 8bit\n"
92-
"Generated-By: Babel {VERSION}\n"
93-
94-
""" + messages
76+
fuzzy_header = '\n#, fuzzy' if fuzzy else ''
77+
return (
78+
"# Translations template for PROJECT.\n"
79+
"# Copyright (C) 1994 ORGANIZATION\n"
80+
"# This file is distributed under the same license as the PROJECT project.\n"
81+
"# FIRST AUTHOR <EMAIL@ADDRESS>, 1994.\n"
82+
"#" + fuzzy_header + "\n"
83+
'msgid ""\n'
84+
'msgstr ""\n'
85+
'"Project-Id-Version: PROJECT VERSION\\n"\n'
86+
'"Report-Msgid-Bugs-To: EMAIL@ADDRESS\\n"\n'
87+
f'"POT-Creation-Date: {date}\\n"\n'
88+
'"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n'
89+
'"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"\n'
90+
'"Language-Team: LANGUAGE <LL@li.org>\\n"\n'
91+
'"MIME-Version: 1.0\\n"\n'
92+
'"Content-Type: text/plain; charset=utf-8\\n"\n'
93+
'"Content-Transfer-Encoding: 8bit\\n"\n'
94+
f'"Generated-By: Babel {VERSION}\\n"\n'
95+
"\n"
96+
) + messages
9597

9698
def test_no_input_files(self):
9799
with pytest.raises(OptionError):
98100
self.cmd.finalize_options()
99101

100102
def test_no_output_file(self):
101103
self.cmd.input_files = ['project/i18n/messages.pot']
104+
self.cmd.finalize_options() # output_file not required; defaults to stdout
105+
106+
def test_unique_exclusive_with_less_than(self):
107+
self.cmd.input_files = [self.temp1, self.temp2]
108+
self.cmd.unique = True
109+
self.cmd.less_than = 3
110+
with pytest.raises(OptionError):
111+
self.cmd.finalize_options()
112+
113+
def test_unique_exclusive_with_more_than(self):
114+
self.cmd.input_files = [self.temp1, self.temp2]
115+
self.cmd.unique = True
116+
self.cmd.more_than = 1
102117
with pytest.raises(OptionError):
103118
self.cmd.finalize_options()
104119

@@ -236,6 +251,7 @@ def test_unique(self):
236251
actual_content = f.read()
237252
assert expected_content == actual_content
238253

254+
self.cmd.unique = False
239255
self.cmd.less_than = 2
240256
self.cmd.finalize_options()
241257
self.cmd.run()

0 commit comments

Comments
 (0)