@@ -291,7 +291,7 @@ def _process_comment(self, line) -> None:
291291 # These are called user comments
292292 self .user_comments .append (line [1 :].strip ())
293293
294- def parse (self , fileobj : IO [AnyStr ]) -> None :
294+ def parse (self , fileobj : IO [AnyStr ] | Iterable [ AnyStr ] ) -> None :
295295 """
296296 Reads from the file-like object `fileobj` and adds any po file
297297 units found in it to the `Catalog` supplied to the constructor.
@@ -329,15 +329,15 @@ def _invalid_pofile(self, line, lineno, msg) -> None:
329329
330330
331331def read_po (
332- fileobj : IO [AnyStr ],
332+ fileobj : IO [AnyStr ] | Iterable [ AnyStr ] ,
333333 locale : str | Locale | None = None ,
334334 domain : str | None = None ,
335335 ignore_obsolete : bool = False ,
336336 charset : str | None = None ,
337337 abort_invalid : bool = False ,
338338) -> Catalog :
339339 """Read messages from a ``gettext`` PO (portable object) file from the given
340- file-like object and return a `Catalog`.
340+ file-like object (or an iterable of lines) and return a `Catalog`.
341341
342342 >>> from datetime import datetime
343343 >>> from io import StringIO
@@ -373,7 +373,7 @@ def read_po(
373373 .. versionadded:: 1.0
374374 Added support for explicit charset argument.
375375
376- :param fileobj: the file-like object to read the PO file from
376+ :param fileobj: the file-like object (or iterable of lines) to read the PO file from
377377 :param locale: the locale identifier or `Locale` object, or `None`
378378 if the catalog is not bound to a locale (which basically
379379 means it's a template)
@@ -529,45 +529,69 @@ def write_po(
529529 updating the catalog
530530 :param include_lineno: include line number in the location comment
531531 """
532- def _normalize (key , prefix = '' ):
533- return normalize (key , prefix = prefix , width = width )
534-
535- def _write (text ):
536- if isinstance (text , str ):
537- text = text .encode (catalog .charset , 'backslashreplace' )
538- fileobj .write (text )
539-
540- def _write_comment (comment , prefix = '' ):
541- # xgettext always wraps comments even if --no-wrap is passed;
542- # provide the same behaviour
543- _width = width if width and width > 0 else 76
544- for line in wraptext (comment , _width ):
545- _write (f"#{ prefix } { line .strip ()} \n " )
546-
547- def _write_message (message , prefix = '' ):
532+
533+ sort_by = None
534+ if sort_output :
535+ sort_by = "message"
536+ elif sort_by_file :
537+ sort_by = "location"
538+
539+ for line in generate_po (
540+ catalog ,
541+ ignore_obsolete = ignore_obsolete ,
542+ include_lineno = include_lineno ,
543+ include_previous = include_previous ,
544+ no_location = no_location ,
545+ omit_header = omit_header ,
546+ sort_by = sort_by ,
547+ width = width ,
548+ ):
549+ if isinstance (line , str ):
550+ line = line .encode (catalog .charset , 'backslashreplace' )
551+ fileobj .write (line )
552+
553+
554+ def generate_po (
555+ catalog : Catalog ,
556+ * ,
557+ ignore_obsolete : bool = False ,
558+ include_lineno : bool = True ,
559+ include_previous : bool = False ,
560+ no_location : bool = False ,
561+ omit_header : bool = False ,
562+ sort_by : Literal ["message" , "location" ] | None = None ,
563+ width : int = 76 ,
564+ ) -> Iterable [str ]:
565+ r"""Yield text strings representing a ``gettext`` PO (portable object) file.
566+
567+ See `write_po()` for a more detailed description.
568+ """
569+ # xgettext always wraps comments even if --no-wrap is passed;
570+ # provide the same behaviour
571+ comment_width = width if width and width > 0 else 76
572+
573+ def _format_comment (comment , prefix = '' ):
574+ for line in wraptext (comment , comment_width ):
575+ yield f"#{ prefix } { line .strip ()} \n "
576+
577+ def _format_message (message , prefix = '' ):
548578 if isinstance (message .id , (list , tuple )):
549579 if message .context :
550- _write ( f"{ prefix } msgctxt { _normalize (message .context , prefix )} \n " )
551- _write ( f"{ prefix } msgid { _normalize (message .id [0 ], prefix )} \n " )
552- _write ( f"{ prefix } msgid_plural { _normalize (message .id [1 ], prefix )} \n " )
580+ yield f"{ prefix } msgctxt { normalize (message .context , prefix = prefix , width = width )} \n "
581+ yield f"{ prefix } msgid { normalize (message .id [0 ], prefix = prefix , width = width )} \n "
582+ yield f"{ prefix } msgid_plural { normalize (message .id [1 ], prefix = prefix , width = width )} \n "
553583
554584 for idx in range (catalog .num_plurals ):
555585 try :
556586 string = message .string [idx ]
557587 except IndexError :
558588 string = ''
559- _write ( f"{ prefix } msgstr[{ idx :d} ] { _normalize (string , prefix )} \n " )
589+ yield f"{ prefix } msgstr[{ idx :d} ] { normalize (string , prefix = prefix , width = width )} \n "
560590 else :
561591 if message .context :
562- _write (f"{ prefix } msgctxt { _normalize (message .context , prefix )} \n " )
563- _write (f"{ prefix } msgid { _normalize (message .id , prefix )} \n " )
564- _write (f"{ prefix } msgstr { _normalize (message .string or '' , prefix )} \n " )
565-
566- sort_by = None
567- if sort_output :
568- sort_by = "message"
569- elif sort_by_file :
570- sort_by = "location"
592+ yield f"{ prefix } msgctxt { normalize (message .context , prefix = prefix , width = width )} \n "
593+ yield f"{ prefix } msgid { normalize (message .id , prefix = prefix , width = width )} \n "
594+ yield f"{ prefix } msgstr { normalize (message .string or '' , prefix = prefix , width = width )} \n "
571595
572596 for message in _sort_messages (catalog , sort_by = sort_by ):
573597 if not message .id : # This is the header "message"
@@ -580,12 +604,12 @@ def _write_message(message, prefix=''):
580604 lines += wraptext (line , width = width ,
581605 subsequent_indent = '# ' )
582606 comment_header = '\n ' .join (lines )
583- _write ( f"{ comment_header } \n " )
607+ yield f"{ comment_header } \n "
584608
585609 for comment in message .user_comments :
586- _write_comment (comment )
610+ yield from _format_comment (comment )
587611 for comment in message .auto_comments :
588- _write_comment (comment , prefix = '.' )
612+ yield from _format_comment (comment , prefix = '.' )
589613
590614 if not no_location :
591615 locs = []
@@ -606,35 +630,34 @@ def _write_message(message, prefix=''):
606630 location = f"{ location } :{ lineno :d} "
607631 if location not in locs :
608632 locs .append (location )
609- _write_comment (' ' .join (locs ), prefix = ':' )
633+ yield from _format_comment (' ' .join (locs ), prefix = ':' )
610634 if message .flags :
611- _write ( f"#{ ', ' .join (['' , * sorted (message .flags )])} \n " )
635+ yield f"#{ ', ' .join (['' , * sorted (message .flags )])} \n "
612636
613637 if message .previous_id and include_previous :
614- _write_comment (
615- f'msgid { _normalize (message .previous_id [0 ])} ' ,
638+ yield from _format_comment (
639+ f'msgid { normalize (message .previous_id [0 ], width = width )} ' ,
616640 prefix = '|' ,
617641 )
618642 if len (message .previous_id ) > 1 :
619- _write_comment ('msgid_plural %s' % _normalize (
620- message .previous_id [1 ],
621- ), prefix = '|' )
643+ norm_previous_id = normalize (message .previous_id [1 ], width = width )
644+ yield from _format_comment (f'msgid_plural { norm_previous_id } ' , prefix = '|' )
622645
623- _write_message (message )
624- _write ( '\n ' )
646+ yield from _format_message (message )
647+ yield '\n '
625648
626649 if not ignore_obsolete :
627650 for message in _sort_messages (
628651 catalog .obsolete .values (),
629652 sort_by = sort_by ,
630653 ):
631654 for comment in message .user_comments :
632- _write_comment (comment )
633- _write_message (message , prefix = '#~ ' )
634- _write ( '\n ' )
655+ yield from _format_comment (comment )
656+ yield from _format_message (message , prefix = '#~ ' )
657+ yield '\n '
635658
636659
637- def _sort_messages (messages : Iterable [Message ], sort_by : Literal ["message" , "location" ]) -> list [Message ]:
660+ def _sort_messages (messages : Iterable [Message ], sort_by : Literal ["message" , "location" ] | None ) -> list [Message ]:
638661 """
639662 Sort the given message iterable by the given criteria.
640663
0 commit comments