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.
Projectpython-babel/babel
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
| 286 | 286 | Locale('de', territory='DE') | |
| 287 | 287 | ||
| 288 | 288 | 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
fail→pass·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.