Whole file

python-babel/babel

The author described this change as Raise a more specific error if `Locale.parse()` is passed an empty value. It counts as a record because the check below fails on the code as it stood at 4d1c8825e and passes on ed4595bb6, with nothing else changed between the two runs.

Fix saved2025-01-09
Sharing licenceBSD-3-Clause · LICENSE
Change size+964 949

What the code was meant to do, written into the code itself as a save note

Raise a more specific error if `Locale.parse()` is passed an empty value

The change

286286 Locale('de', territory='DE')
287287
288288 If the `identifier` parameter is neither of these, such as `None`
289- e.g. because a default locale identifier could not be determined,
290- a `TypeError` is raised:
291-
292- >>> Locale.parse(None)
293- Traceback (most recent call last):
294- ...
295- TypeError: ...
296-
297- This also can perform resolving of likely subtags which it does
298- by default. This is for instance useful to figure out the most
299- likely locale for a territory you can use ``'und'`` as the
300- language tag:
301-
302- >>> Locale.parse('und_AT')
303- Locale('de', territory='AT')
304-
305- Modifiers are optional, and always at the end, separated by "@":
306-
307- >>> Locale.parse('de_AT@euro')
308- Locale('de', territory='AT', modifier='euro')
309-
310- :param identifier: the locale identifier string
311- :param sep: optional component separator
312- :param resolve_likely_subtags: if this is specified then a locale will
313- have its likely subtag resolved if the
314- locale otherwise does not exist. For
315- instance ``zh_TW`` by itself is not a
316- locale that exists but Babel can
317- automatically expand it to the full
318- form of ``zh_hant_TW``. Note that this
319- expansion is only taking place if no
320- locale exists otherwise. For instance
321- there is a locale ``en`` that can exist
322- by itself.
323- :raise `ValueError`: if the string does not appear to be a valid locale
324- identifier
325- :raise `UnknownLocaleError`: if no locale data is available for the
326- requested locale
327- :raise `TypeError`: if the identifier is not a string or a `Locale`
328- """
329- if isinstance(identifier, Locale):
330- return identifier
331- elif not isinstance(identifier, str):
332- raise TypeError(f"Unexpected value for identifier: {identifier!r}")
333-
334- parts = parse_locale(identifier, sep=sep)
335- input_id = get_locale_identifier(parts)
336-
337- def _try_load(parts):
338- try:
339- return cls(*parts)
340- except UnknownLocaleError:
341- return None
342-
343- def _try_load_reducing(parts):
344- # Success on first hit, return it.
345- locale = _try_load(parts)
346- if locale is not None:
347- return locale
348-
349- # Now try without script and variant
350- locale = _try_load(parts[:2])
351- if locale is not None:
352- return locale
353-
354- locale = _try_load(parts)
355- if locale is not None:
356- return locale
357- if not resolve_likely_subtags:
358- raise UnknownLocaleError(input_id)
359-
360- # From here onwards is some very bad likely subtag resolving. This
361- # whole logic is not entirely correct but good enough (tm) for the
362- # time being. This has been added so that zh_TW does not cause
363- # errors for people when they upgrade. Later we should properly
364- # implement ICU like fuzzy locale objects and provide a way to
365- # maximize and minimize locale tags.
366-
367- if len(parts) == 5:
368- language, territory, script, variant, modifier = parts
369- else:
370- language, territory, script, variant = parts
371- modifier = None
372- language = get_global('language_aliases').get(language, language)
373- territory = get_global('territory_aliases').get(territory or '', (territory,))[0]
374- script = get_global('script_aliases').get(script or '', script)
375- variant = get_global('variant_aliases').get(variant or '', variant)
376-
377- if territory == 'ZZ':
378- territory = None
379- if script == 'Zzzz':
380- script = None
381-
382- parts = language, territory, script, variant, modifier
383-
384- # First match: try the whole identifier
385- new_id = get_locale_identifier(parts)
386- likely_subtag = get_global('likely_subtags').get(new_id)
387- if likely_subtag is not None:
388- locale = _try_load_reducing(parse_locale(likely_subtag))
389- if locale is not None:
390- return locale
391-
392- # If we did not find anything so far, try again with a
393- # simplified identifier that is just the language
394- likely_subtag = get_global('likely_subtags').get(language)
395- if likely_subtag is not None:
396- parts2 = parse_locale(likely_subtag)
397- if len(parts2) == 5:
398- language2, _, script2, variant2, modifier2 = parts2
399- else:
400- language2, _, script2, variant2 = parts2
401- modifier2 = None
402- locale = _try_load_reducing((language2, territory, script2, variant2, modifier2))
403- if locale is not None:
404- return locale
405-
406- raise UnknownLocaleError(input_id)
407-
408- def __eq__(self, other: object) -> bool:
409- for key in ('language', 'territory', 'script', 'variant', 'modifier'):
410- if not hasattr(other, key):
411- return False
412- return (
413- self.language == getattr(other, 'language') and # noqa: B009
414- self.territory == getattr(other, 'territory') and # noqa: B009
415- self.script == getattr(other, 'script') and # noqa: B009
416- self.variant == getattr(other, 'variant') and # noqa: B009
417- self.modifier == getattr(other, 'modifier') # noqa: B009
418- )
419-
420- def __ne__(self, other: object) -> bool:
421- return not self.__eq__(other)
422-
423- def __hash__(self) -> int:
424- return hash((self.language, self.territory, self.script,
425- self.variant, self.modifier))
426-
427- def __repr__(self) -> str:
428- parameters = ['']
429- for key in ('territory', 'script', 'variant', 'modifier'):
430- value = getattr(self, key)
431- if value is not None:
432- parameters.append(f"{key}={value!r}")
433- return f"Locale({self.language!r}{', '.join(parameters)})"
434-
435- def __str__(self) -> str:
436- return get_locale_identifier((self.language, self.territory,
437- self.script, self.variant,
438- self.modifier))
439-
440- @property
441- def _data(self) -> localedata.LocaleDataDict:
442- if self.__data is None:
443- self.__data = localedata.LocaleDataDict(localedata.load(self.__data_identifier))
444- return self.__data
445-
446- def get_display_name(self, locale: Locale | str | None = None) -> str | None:
447- """Return the display name of the locale using the given locale.
448-
449- The display name will include the language, territory, script, and
450- variant, if those are specified.
451-
452- >>> Locale('zh', 'CN', script='Hans').get_display_name('en')
453- u'Chinese (Simplified, China)'
454-
455- Modifiers are currently passed through verbatim:
456-
457- >>> Locale('it', 'IT', modifier='euro').get_display_name('en')
458- u'Italian (Italy, euro)'
459-
460- :param locale: the locale to use
461- """
462- if locale is None:
463- locale = self
464- locale = Locale.parse(locale)
465- retval = locale.languages.get(self.language)
466- if retval and (self.territory or self.script or self.variant):
467- details = []
468- if self.script:
469- details.append(locale.scripts.get(self.script))
470- if self.territory:
471- details.append(locale.territories.get(self.territory))
472- if self.variant:
473- details.append(locale.variants.get(self.variant))
474- if self.modifier:
475- details.append(self.modifier)
476- detail_string = ', '.join(atom for atom in details if atom)
477- if detail_string:
478- retval += f" ({detail_string})"
479- return retval
480-
481- display_name = property(get_display_name, doc="""\
482- The localized display name of the locale.
483-
484- >>> Locale('en').display_name
485- u'English'
486- >>> Locale('en', 'US').display_name
487- u'English (United States)'
488- >>> Locale('sv').display_name
489- u'svenska'
490-
491- :type: `unicode`
492- """)
493-
494- def get_language_name(self, locale: Locale | str | None = None) -> str | None:
495- """Return the language of this locale in the given locale.
496-
497- >>> Locale('zh', 'CN', script='Hans').get_language_name('de')
498- u'Chinesisch'
499-
500- .. versionadded:: 1.0
501-
502- :param locale: the locale to use
503- """
504- if locale is None:
505- locale = self
506- locale = Locale.parse(locale)
507- return locale.languages.get(self.language)
508-
509- language_name = property(get_language_name, doc="""\
510- The localized language name of the locale.
511-
512- >>> Locale('en', 'US').language_name
513- u'English'
514- """)
515-
516- def get_territory_name(self, locale: Locale | str | None = None) -> str | None:
517- """Return the territory name in the given locale."""
518- if locale is None:
519- locale = self
520- locale = Locale.parse(locale)
521- return locale.territories.get(self.territory or '')
522-
523- territory_name = property(get_territory_name, doc="""\
524- The localized territory name of the locale if available.
525-
526- >>> Locale('de', 'DE').territory_name
527- u'Deutschland'
528- """)
529-
530- def get_script_name(self, locale: Locale | str | None = None) -> str | None:
531- """Return the script name in the given locale."""
532- if locale is None:
533- locale = self
534- locale = Locale.parse(locale)
535- return locale.scripts.get(self.script or '')
536-
537- script_name = property(get_script_name, doc="""\
538- The localized script name of the locale if available.
539-
540- >>> Locale('sr', 'ME', script='Latn').script_name
541- u'latinica'
542- """)
543-
544- @property
545- def english_name(self) -> str | None:
546- """The english display name of the locale.
547-
548- >>> Locale('de').english_name
549- u'German'
550- >>> Locale('de', 'DE').english_name
551- u'German (Germany)'
552-
553- :type: `unicode`"""
554- return self.get_display_name(Locale('en'))
555-
556- # { General Locale Display Names
557-
558- @property
559- def languages(self) -> localedata.LocaleDataDict:
560- """Mapping of language codes to translated language names.
561-
562- >>> Locale('de', 'DE').languages['ja']
563- u'Japanisch'
564-
565- See `ISO 639 <http://www.loc.gov/standards/iso639-2/>`_ for
566- more information.
567- """
568- return self._data['languages']
569-
570- @property
571- def scripts(self) -> localedata.LocaleDataDict:
572- """Mapping of script codes to translated script names.
573-
574- >>> Locale('en', 'US').scripts['Hira']
575- u'Hiragana'
576-
577- See `ISO 15924 <http://www.evertype.com/standards/iso15924/>`_
578- for more information.
579- """
580- return self._data['scripts']
581-
582- @property
583- def territories(self) -> localedata.LocaleDataDict:
584- """Mapping of script codes to translated script names.
585-
586- >>> Locale('es', 'CO').territories['DE']
587- u'Alemania'
588-
589- See `ISO 3166 <http://www.iso.org/iso/en/prods-services/iso3166ma/>`_
590- for more information.
591- """
592- return self._data['territories']
593-
594- @property
595- def variants(self) -> localedata.LocaleDataDict:
596- """Mapping of script codes to translated script names.
597-
598- >>> Locale('de', 'DE').variants['1901']
599- u'Alte deutsche Rechtschreibung'
600- """
601- return self._data['variants']
602-
603- # { Number Formatting
604-
605- @property
606- def currencies(self) -> localedata.LocaleDataDict:
607- """Mapping of currency codes to translated currency names. This
608- only returns the generic form of the currency name, not the count
609- specific one. If an actual number is requested use the
610- :func:`babel.numbers.get_currency_name` function.
611-
612- >>> Locale('en').currencies['COP']
613- u'Colombian Peso'
614- >>> Locale('de', 'DE').currencies['COP']
615- u'Kolumbianischer Peso'
616- """
617- return self._data['currency_names']
618-
619- @property
620- def currency_symbols(self) -> localedata.LocaleDataDict:
621- """Mapping of currency codes to symbols.
622-
623- >>> Locale('en', 'US').currency_symbols['USD']
624- u'$'
625- >>> Locale('es', 'CO').currency_symbols['USD']
626- u'US$'
627- """
628- return self._data['currency_symbols']
629-
630- @property
631- def number_symbols(self) -> localedata.LocaleDataDict:
632- """Symbols used in number formatting by number system.
633-
634- .. note:: The format of the value returned may change between
635- Babel versions.
636-
637- >>> Locale('fr', 'FR').number_symbols["latn"]['decimal']
638- u','
639- >>> Locale('fa', 'IR').number_symbols["arabext"]['decimal']
640- u'٫'
641- >>> Locale('fa', 'IR').number_symbols["latn"]['decimal']
642- u'.'
643- """
644- return self._data['number_symbols']
645-
646- @property
647- def other_numbering_systems(self) -> localedata.LocaleDataDict:
648- """
649- Mapping of other numbering systems available for the locale.
650- See: https://www.unicode.org/reports/tr35/tr35-numbers.html#otherNumberingSystems
651-
652- >>> Locale('el', 'GR').other_numbering_systems['traditional']
653- u'grek'
654-
655- .. note:: The format of the value returned may change between
656- Babel versions.
657- """
658- return self._data['numbering_systems']
659-
660- @property
661- def default_numbering_system(self) -> str:
662- """The default numbering system used by the locale.
663- >>> Locale('el', 'GR').default_numbering_system
664- u'latn'
665- """
666- return self._data['default_numbering_system']
667-
668- @property
669- def decimal_formats(self) -> localedata.LocaleDataDict:
670- """Locale patterns for decimal number formatting.
671-
672- .. note:: The format of the value returned may change between
673- Babel versions.
674-
675- >>> Locale('en', 'US').decimal_formats[None]
676- <NumberPattern u'#,##0.###'>
677- """
678- return self._data['decimal_formats']
679-
680- @property
681- def compact_decimal_formats(self) -> localedata.LocaleDataDict:
682- """Locale patterns for compact decimal number formatting.
683-
684- .. note:: The format of the value returned may change between
685- Babel versions.
1519 further changed lines not shown

The check that tells the two apart

failpass·tests/test_core.py::test_locale_parse_empty

Check file tests/test_core.py, taken without changes from the fix and copied onto the older code, so the exact same check runs against both versions.

Origin and history

The code before it4d1c8825e80ec91534c4c1ccc0108a323c23638c
Broken version dated2025-01-09
Modulebabel.core
Units changedLocale, parse_locale
Fingerprintf845f427beaaeb7b
Checked2026-08-18 by goldset/0.1

Every field above is generated by our program. None of it is written by hand.

Other bugs found in python-babel/babel