From 763ca5aeafe4aaf6eb7ee6329ab5b7b3402a2bb4 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 9 Jun 2026 08:18:43 -0400 Subject: [PATCH 1/7] First cut adding AlphabeticOrder options --- pymathics/icu/__init__.py | 3 +- pymathics/icu/__main__.py | 212 +++++++++++++++++++++++++++----------- pymathics/icu/version.py | 2 +- 3 files changed, 152 insertions(+), 65 deletions(-) diff --git a/pymathics/icu/__init__.py b/pymathics/icu/__init__.py index 5e78af5..74fe247 100644 --- a/pymathics/icu/__init__.py +++ b/pymathics/icu/__init__.py @@ -26,7 +26,7 @@ = {ʼ, а, б, в, г, д, е, ж, з, и, й, к, л, м, н, о, п, р, с, т, у, ф, х, ц, ч, ш, щ, ь, ю, я, є, і, ї, ґ} """ -from pymathics.icu.__main__ import Alphabet, AlphabeticOrder, Language +from pymathics.icu.__main__ import Alphabet, AlphabeticOrder from pymathics.icu.version import __version__ pymathics_version_data = { @@ -39,7 +39,6 @@ __all__ = [ "Alphabet", "AlphabeticOrder", - "Language", "pymathics_version_data", "__version__", ] diff --git a/pymathics/icu/__main__.py b/pymathics/icu/__main__.py index 45aac4c..37ba11d 100644 --- a/pymathics/icu/__main__.py +++ b/pymathics/icu/__main__.py @@ -4,13 +4,15 @@ Languages - Human-Language Alphabets and Locales via PyICU. """ -from typing import List, Optional - -from icu import Collator, Locale, LocaleData +from dataclasses import dataclass +from icu import Collator, Locale, LocaleData, UCollAttribute, UCollAttributeValue +from typing import Any, Optional +from mathics.builtin.system import LANGUAGE from mathics.core.atoms import Integer, String -from mathics.core.builtin import Builtin, Predefined +from mathics.core.builtin import Builtin from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation +from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue available_locales = Locale.getAvailableLocales() language2locale = { @@ -18,11 +20,87 @@ for locale_name, availableLocale in available_locales.items() } -# The current value of $Language -LANGUAGE = "English" +SymbolLanguage = Symbol("System`$Language") +@dataclass(frozen=True) +class AlphabeticOrderOptions: + """ + Stores options associated with AlphbeticOrder[] builtin. -def eval_alphabet(language_name: String) -> Optional[List[String]]: + One initialized, this structure is immutable or frozen. + """ + + # case_ordering: bool=False + # """How to order upper versus lower case""" + + ignore_case: bool=False + """How to order upper versus lower case""" + + ignore_diacritics: bool=False + """whether to ignore diacritics for ordering""" + + ignore_punctuation: bool=False + """whether to ignore punctuation for ordering""" + + language: str=LANGUAGE + """what language or alphabet to assume""" + + @classmethod + def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": + """Factory method that normalizes, type-checks, and builds the frozen structure + from a raw dict[str, str]. + """ + key_mapping = { + # "System`CaseOrdering": "ignore_case", + "System`IgnoreCase": "ignore_case", + "System`IgnoreDiacritics": "ignore_diacritics", + "System`IgnorePunctuation": "ignore_punctuation", + "System`Language": "language", + } + + # This will hold our cleaned, type-converted parameters + processed_args: dict[str, Any] = { + # "case_ordering": False, + "ignore_case": False, + "ignore_diacritics": False, + "ignore_punctuation": False, + "language": LANGUAGE, + } + + # Iterate through the user-provided options dictionary + for raw_key, option_value in options.items(): + normalized_key = key_mapping.get(raw_key) + + if not normalized_key: + raise TypeError( + f"Unknown option field provided: '{raw_key}'" + ) + + # Type parsing and validation based on the target field name + if normalized_key in ("ignore_case", "ignore_diacritics", "ignore_punctuation"): + if option_value not in (SymbolTrue, SymbolFalse): + raise TypeError( + f"Field '{raw_key}' expects a Boolean value. " + f"Got: '{option_value}'" + ) + processed_args[normalized_key] = option_value.value + + if normalized_key == "language": + if option_value is SymbolLanguage: + option_value = String(LANGUAGE) + + if not isinstance(option_value, String): + raise TypeError( + f"Field '{raw_key}' expects a String value. " + f"Got: '{option_value}'" + ) + processed_args[normalized_key] = option_value + + # Initialize and return the frozen dataclass using our verified arguments + return cls(**processed_args) + + +def eval_alphabet(language_name: String) -> Optional[list[String]]: py_language_name = language_name.value locale = language2locale.get(py_language_name, py_language_name) @@ -32,7 +110,7 @@ def eval_alphabet(language_name: String) -> Optional[List[String]]: return to_mathics_list(*alphabet_set, elements_conversion_fn=String) -def eval_alphabetic_order(string1: str, string2: str, language_name=LANGUAGE) -> int: +def eval_alphabetic_order(string1: str, string2: str, language_name, options: AlphabeticOrderOptions) -> int: """ Compare two strings using locale-sensitive alphabetic order. @@ -43,6 +121,45 @@ def eval_alphabetic_order(string1: str, string2: str, language_name=LANGUAGE) -> """ locale_str = language_to_locale(language_name) collator = Collator.createInstance(Locale(locale_str)) + + # Configure Case and Diacritic (Accent) rules via Collator Strength + # - PRIMARY: Only looks at the base letter (ignores case AND accents). + # - SECONDARY: Looks at base letters + accents (ignores case). + # - TERTIARY: Looks at base letters + accents + case (Default strict sorting). + + if options.ignore_case and options.ignore_diacritics: + # Ignore both accent variations and case sizes + collator.setStrength(Collator.PRIMARY) + + elif options.ignore_case and not options.ignore_diacritics: + # Ignore upper vs lower case, but treat 'e' and 'é' as different letters + collator.setStrength(Collator.SECONDARY) + + elif not options.ignore_case and options.ignore_diacritics: + # Ignore accents, but treat 'A' and 'a' as different letters. + # ICU handles this by setting strength to PRIMARY but turning on Case Level. + collator.setStrength(Collator.PRIMARY) + collator.setAttribute(UCollAttribute.CASE_LEVEL, UCollAttributeValue.ON) + + else: + # Default: strict matching on both case and diacritics + collator.setStrength(Collator.TERTIARY) + + # Configure Punctuation ignoring + # In ICU, ignoring punctuation is called "Alternate Handling". Turning it + # to SHIFTED moves punctuation tokens to the very end of the weight table, + # effectively ignoring them during normal alphanumeric string comparison. + if options.ignore_punctuation: + collator.setAttribute( + UCollAttribute.ALTERNATE_HANDLING, + UCollAttributeValue.SHIFTED + ) + else: + collator.setAttribute( + UCollAttribute.ALTERNATE_HANDLING, + UCollAttributeValue.NON_IGNORABLE + ) + comparison = collator.compare(string1, string2) if comparison < 0: return 1 @@ -133,6 +250,11 @@ class AlphabeticOrder(Builtin):
gives 1 if $string_1$ appears before $string_2$ in alphabetical order, -1 if it is after, and 0 if it is identical. + The alphabetic order of two characters: + >> AlphabeticOrder["e", "f"] + = 1 + + The alphabetic order of two strings: >> AlphabeticOrder["apple", "banana"] = 1 @@ -155,68 +277,34 @@ class AlphabeticOrder(Builtin): = 1 """ - summary_text = "compare strings according to an alphabet" + eval_error = Builtin.generic_argument_error + expected_args = range(1, 4) + options = { + # "System`CaseOrdering": "Automatic", + "System`IgnoreCase": "False", + "System`IgnoreDiacritics": "False", + "System`IgnorePunctuation": "False", + "System`Language": "$Language", + } + summary_text = "return -1, 0, 1 comparing the alphabetic order of two strings" - def eval(self, string1: String, string2: String, evaluation: Evaluation): - """AlphabeticOrder[string1_String, string2_String]""" - return Integer(eval_alphabetic_order(string1.value, string2.value)) + def eval(self, string1: String, string2: String, evaluation: Evaluation, options: dict): + """AlphabeticOrder[string1_String, string2_String, OptionsPattern[%(name)s]]""" + lang = String(LANGUAGE) + return self.eval_with_lang(string1, string2, lang, options, evaluation) def eval_with_lang( - self, string1: String, string2: String, lang: String, evaluation: Evaluation + self, string1: String, string2: String, lang: String, options: dict, evaluation: Evaluation ): - """AlphabeticOrder[string1_String, string2_String, lang_String]""" + """AlphabeticOrder[string1_String, string2_String, lang_String, OptionsPattern[%(name)s]]""" + + alphabetic_order_options = AlphabeticOrderOptions.from_dict(options) + return Integer( eval_alphabetic_order( string1.value, string2.value, lang.value, + alphabetic_order_options, ) ) - - -## FIXME: move to mathics-core. Will have to change references to Pymathics`$Language to $Language -class Language(Predefined): - """ - - :WMA link: - https://reference.wolfram.com/language/ref/\\$Language.html - -
-
'\\$Language' -
is a settable global variable for the default language used in Mathics3. -
- - See the language in effect used for functions like 'Alphabet[]': - - By setting its value, The letters of 'Alphabet[]' are changed: - - >> $Language = "German"; Alphabet[] - = ... - - #> $Language = "English" - = English - - See also - :Alphabet: - /doc/mathics3-modules/icu-international-components-for-unicode/languages-human-language-alphabets-and-locales-via-pyicu/alphabet/. - """ - - name = "$Language" - messages = { - "notstr": "`1` is not a string. Only strings can be set as the value of $Language.", - } - - summary_text = "settable global variable giving the default language" - value = f'"{LANGUAGE}"' - # Rules has to come after "value" - rules = { - "Pymathics`$Language": value, - } - - def eval_set(self, value, evaluation: Evaluation): - """Set[Pymathics`$Language, value_]""" - if isinstance(value, String): - evaluation.definitions.set_ownvalue("$Language", value) - else: - evaluation.message("Pymathics`$Language", "notstr", value) - return value diff --git a/pymathics/icu/version.py b/pymathics/icu/version.py index e2fcf46..cd2e848 100644 --- a/pymathics/icu/version.py +++ b/pymathics/icu/version.py @@ -5,4 +5,4 @@ # well as importing into Python. That's why there is no # space around "=" below. # fmt: off -__version__="10.0.0" # noqa +__version__="10.0.1.dev0" # noqa From 2b2452b3218773598325fb9d95fdeb53d0d7a5f6 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 9 Jun 2026 09:27:18 -0400 Subject: [PATCH 2/7] Handle "Automatic" option for AlphbeticOrder --- pymathics/icu/__main__.py | 40 ++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/pymathics/icu/__main__.py b/pymathics/icu/__main__.py index 37ba11d..cea7adc 100644 --- a/pymathics/icu/__main__.py +++ b/pymathics/icu/__main__.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from icu import Collator, Locale, LocaleData, UCollAttribute, UCollAttributeValue -from typing import Any, Optional +from typing import Any, Final, Optional from mathics.builtin.system import LANGUAGE from mathics.core.atoms import Integer, String from mathics.core.builtin import Builtin @@ -20,7 +20,10 @@ for locale_name, availableLocale in available_locales.items() } -SymbolLanguage = Symbol("System`$Language") +StringAutomatic: Final[String] = String("Automatic") +StringLowerFirst: Final[String] = String("LowerFirst") +StringUpperFirst: Final[String] = String("UpperFirst") +SymbolLanguage: Final[String] = Symbol("System`$Language") @dataclass(frozen=True) class AlphabeticOrderOptions: @@ -30,11 +33,12 @@ class AlphabeticOrderOptions: One initialized, this structure is immutable or frozen. """ - # case_ordering: bool=False - # """How to order upper versus lower case""" + lowercase_ordering: Optional[bool]=None + """'True" if ordering should be lowercase first, 'False" if should uppercase first, + and 'None' if we should use the natural alphabet ordering case.""" ignore_case: bool=False - """How to order upper versus lower case""" + """whether to ignore upper versus lower case""" ignore_diacritics: bool=False """whether to ignore diacritics for ordering""" @@ -51,7 +55,7 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": from a raw dict[str, str]. """ key_mapping = { - # "System`CaseOrdering": "ignore_case", + "System`CaseOrdering": "lowercase_ordering", "System`IgnoreCase": "ignore_case", "System`IgnoreDiacritics": "ignore_diacritics", "System`IgnorePunctuation": "ignore_punctuation", @@ -60,7 +64,7 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": # This will hold our cleaned, type-converted parameters processed_args: dict[str, Any] = { - # "case_ordering": False, + "lowercase_ordering": None, "ignore_case": False, "ignore_diacritics": False, "ignore_punctuation": False, @@ -85,7 +89,7 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": ) processed_args[normalized_key] = option_value.value - if normalized_key == "language": + elif normalized_key == "language": if option_value is SymbolLanguage: option_value = String(LANGUAGE) @@ -96,6 +100,19 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": ) processed_args[normalized_key] = option_value + elif normalized_key == "lowercase_ordering": + if option_value == StringAutomatic: + processed_args[normalized_key] = None + elif option_value == StringLowerFirst: + processed_args[normalized_key] = True + elif option_value == StringUpperFirst: + processed_args[normalized_key] = False + else: + raise TypeError( + f"Field 'CaseOrdering' expects a 'UpperFirst', 'LowerFirst' or Automatic" + f"Got: '{option_value}'" + ) + # Initialize and return the frozen dataclass using our verified arguments return cls(**processed_args) @@ -160,6 +177,11 @@ def eval_alphabetic_order(string1: str, string2: str, language_name, options: Al UCollAttributeValue.NON_IGNORABLE ) + if options.lowercase_ordering: + collator.setAttribute(UCollAttribute.CASE_FIRST, UCollAttributeValue.LOWER_FIRST) + elif options.lowercase_ordering is False: + collator.setAttribute(UCollAttribute.CASE_FIRST, UCollAttributeValue.UPPER_FIRST) + comparison = collator.compare(string1, string2) if comparison < 0: return 1 @@ -280,7 +302,7 @@ class AlphabeticOrder(Builtin): eval_error = Builtin.generic_argument_error expected_args = range(1, 4) options = { - # "System`CaseOrdering": "Automatic", + "System`CaseOrdering": "Automatic", "System`IgnoreCase": "False", "System`IgnoreDiacritics": "False", "System`IgnorePunctuation": "False", From 4d4fe3271ec69116b672345b853737c787312144 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 9 Jun 2026 11:48:48 -0400 Subject: [PATCH 3/7] Add IgnorePunctuation option to AddAlphabeticOrder --- pymathics/icu/__main__.py | 94 ++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/pymathics/icu/__main__.py b/pymathics/icu/__main__.py index cea7adc..299bdb1 100644 --- a/pymathics/icu/__main__.py +++ b/pymathics/icu/__main__.py @@ -5,14 +5,16 @@ """ from dataclasses import dataclass -from icu import Collator, Locale, LocaleData, UCollAttribute, UCollAttributeValue from typing import Any, Final, Optional + +from icu import Collator, Locale, LocaleData, UCollAttribute, UCollAttributeValue from mathics.builtin.system import LANGUAGE from mathics.core.atoms import Integer, String from mathics.core.builtin import Builtin from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue +from mathics.core.systemsymbols import SymbolAutomatic available_locales = Locale.getAvailableLocales() language2locale = { @@ -20,11 +22,12 @@ for locale_name, availableLocale in available_locales.items() } -StringAutomatic: Final[String] = String("Automatic") -StringLowerFirst: Final[String] = String("LowerFirst") +StringAutomatic: Final[String] = String("System`Automatic") +LowerFirst: Final[set[String]] = {String("System`LowerFirst"), String("LowerFirst")} StringUpperFirst: Final[String] = String("UpperFirst") SymbolLanguage: Final[String] = Symbol("System`$Language") + @dataclass(frozen=True) class AlphabeticOrderOptions: """ @@ -33,20 +36,20 @@ class AlphabeticOrderOptions: One initialized, this structure is immutable or frozen. """ - lowercase_ordering: Optional[bool]=None + lowercase_ordering: Optional[bool] = None """'True" if ordering should be lowercase first, 'False" if should uppercase first, and 'None' if we should use the natural alphabet ordering case.""" - ignore_case: bool=False + ignore_case: bool = False """whether to ignore upper versus lower case""" - ignore_diacritics: bool=False + ignore_diacritics: bool = False """whether to ignore diacritics for ordering""" - ignore_punctuation: bool=False + ignore_punctuation: bool = False """whether to ignore punctuation for ordering""" - language: str=LANGUAGE + language: str = LANGUAGE """what language or alphabet to assume""" @classmethod @@ -76,12 +79,14 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": normalized_key = key_mapping.get(raw_key) if not normalized_key: - raise TypeError( - f"Unknown option field provided: '{raw_key}'" - ) + raise TypeError(f"Unknown option field provided: '{raw_key}'") # Type parsing and validation based on the target field name - if normalized_key in ("ignore_case", "ignore_diacritics", "ignore_punctuation"): + if normalized_key in ( + "ignore_case", + "ignore_diacritics", + "ignore_punctuation", + ): if option_value not in (SymbolTrue, SymbolFalse): raise TypeError( f"Field '{raw_key}' expects a Boolean value. " @@ -101,15 +106,16 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": processed_args[normalized_key] = option_value elif normalized_key == "lowercase_ordering": - if option_value == StringAutomatic: + if (option_value is SymbolAutomatic) or option_value == "Automatic": processed_args[normalized_key] = None - elif option_value == StringLowerFirst: + elif option_value in LowerFirst: processed_args[normalized_key] = True elif option_value == StringUpperFirst: processed_args[normalized_key] = False else: + breakpoint() raise TypeError( - f"Field 'CaseOrdering' expects a 'UpperFirst', 'LowerFirst' or Automatic" + f"Field 'CaseOrdering' expects a 'UpperFirst', 'LowerFirst' or Automatic " f"Got: '{option_value}'" ) @@ -127,7 +133,9 @@ def eval_alphabet(language_name: String) -> Optional[list[String]]: return to_mathics_list(*alphabet_set, elements_conversion_fn=String) -def eval_alphabetic_order(string1: str, string2: str, language_name, options: AlphabeticOrderOptions) -> int: +def eval_alphabetic_order( + string1: str, string2: str, language_name, options: AlphabeticOrderOptions +) -> int: """ Compare two strings using locale-sensitive alphabetic order. @@ -168,19 +176,21 @@ def eval_alphabetic_order(string1: str, string2: str, language_name, options: Al # effectively ignoring them during normal alphanumeric string comparison. if options.ignore_punctuation: collator.setAttribute( - UCollAttribute.ALTERNATE_HANDLING, - UCollAttributeValue.SHIFTED + UCollAttribute.ALTERNATE_HANDLING, UCollAttributeValue.SHIFTED ) else: collator.setAttribute( - UCollAttribute.ALTERNATE_HANDLING, - UCollAttributeValue.NON_IGNORABLE + UCollAttribute.ALTERNATE_HANDLING, UCollAttributeValue.NON_IGNORABLE ) if options.lowercase_ordering: - collator.setAttribute(UCollAttribute.CASE_FIRST, UCollAttributeValue.LOWER_FIRST) + collator.setAttribute( + UCollAttribute.CASE_FIRST, UCollAttributeValue.LOWER_FIRST + ) elif options.lowercase_ordering is False: - collator.setAttribute(UCollAttribute.CASE_FIRST, UCollAttributeValue.UPPER_FIRST) + collator.setAttribute( + UCollAttribute.CASE_FIRST, UCollAttributeValue.UPPER_FIRST + ) comparison = collator.compare(string1, string2) if comparison < 0: @@ -250,7 +260,7 @@ class Alphabet(Builtin): } rules = { - "Alphabet[]": """Alphabet[Pymathics`$Language]""", + "Alphabet[]": """Alphabet[$Language]""", } summary_text = "lowercase letters in an alphabet" @@ -287,6 +297,16 @@ class AlphabeticOrder(Builtin): >> AlphabeticOrder["A", "a"] = -1 + However, you can for which case comes first using the 'CaseOrdering' option: + >> AlphabeticOrder["a", "A", CaseOrdering -> "LowerFirst"] + = 1 + + >> AlphabeticOrder["a", "A", CaseOrdering -> "UpperFirst"] + = -1 + + >> AlphabeticOrder["a", "A"] == AlphabeticOrder["a", "A", CaseOrdering -> "LowerFirst"] + = True + Longer words follow their prefixes: >> AlphabeticOrder["Papagayo", "Papa", "Spanish"] = -1 @@ -297,6 +317,23 @@ class AlphabeticOrder(Builtin): >> AlphabeticOrder["Papá", "Papagayo", "Spanish"] = 1 + + The alphabetic ordering is determined by the value of :$Language: + doc/reference-of-built-in-symbols/global-system-information/$language/. However, \ + specify a the language as the third argument: + >> AlphabeticOrder["ñ", "n", "Spanish"] + = -1 + + Option 'IgnorePunctuation' specifies whether to remove puctuation characters before comparing the strings: + + >> AlphabeticOrder["Name-1", "Name.1", "Spanish", IgnorePunctuation -> True] + = 0 + + >> AlphabeticOrder["it's", "its", "English", IgnorePunctuation -> False] + = 1 + + >> AlphabeticOrder["it's", "its", "English", IgnorePunctuation -> True] + = 0 """ eval_error = Builtin.generic_argument_error @@ -310,13 +347,20 @@ class AlphabeticOrder(Builtin): } summary_text = "return -1, 0, 1 comparing the alphabetic order of two strings" - def eval(self, string1: String, string2: String, evaluation: Evaluation, options: dict): + def eval( + self, string1: String, string2: String, evaluation: Evaluation, options: dict + ): """AlphabeticOrder[string1_String, string2_String, OptionsPattern[%(name)s]]""" lang = String(LANGUAGE) return self.eval_with_lang(string1, string2, lang, options, evaluation) def eval_with_lang( - self, string1: String, string2: String, lang: String, options: dict, evaluation: Evaluation + self, + string1: String, + string2: String, + lang: String, + options: dict, + evaluation: Evaluation, ): """AlphabeticOrder[string1_String, string2_String, lang_String, OptionsPattern[%(name)s]]""" From f181c261a0d85344c8fb3f3d2bf991e7591b6435 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 9 Jun 2026 11:59:36 -0400 Subject: [PATCH 4/7] YML lint config file --- .github/workflows/isort-and-black-checks.yml | 42 ++++++++++---------- .pre-commit-config.yaml | 25 ++++++------ 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/.github/workflows/isort-and-black-checks.yml b/.github/workflows/isort-and-black-checks.yml index 1273de1..00bf1dc 100644 --- a/.github/workflows/isort-and-black-checks.yml +++ b/.github/workflows/isort-and-black-checks.yml @@ -1,32 +1,34 @@ -# GitHub Action that uses Black to reformat the Python code in an incoming pull request. -# If all Python code in the pull request is compliant with Black then this Action does nothing. -# Othewrwise, Black is run and its changes are committed back to the incoming pull request. -# https://github.com/cclauss/autoblack +# GitHub Action that uses Black to reformat the Python code in an +# incoming pull request. If all Python code in the pull request is +# compliant with Black then this Action does nothing. Othewrwise, +# Black is run and its changes are committed back to the incoming pull +# request. https://github.com/cclauss/autoblack +--- name: isort and black check on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python 3.14 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 - name: Install click, black and isort - run: pip install 'click==8.0.4' 'black==25.1.0' 'isort==5.13.2' + run: pip install 'click==8.2.1' 'black==25.11.0' 'isort==8.0.1' - name: Run isort --check . run: isort --check . - - name: Run black --check . - run: black --check . - # - name: If needed, commit black changes to the pull request - # if: failure() - # run: | - # black . - # git config --global user.name 'autoblack' - # git config --global user.email 'rocky@users.noreply.github.com' - # git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY - # git checkout $GITHUB_HEAD_REF - # git commit -am "fixup: Format Python code with Black" - # git push + - name: Run black --check --diff . + run: black --check --diff . + # - name: If needed, commit black changes to the pull request + # if: failure() + # run: | + # black . + # git config --global user.name 'autoblack' + # git config --global user.email 'rocky@users.noreply.github.com' + # git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY + # git checkout $GITHUB_HEAD_REF + # git commit -am "fixup: Format Python code with Black" + # git push diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e4d9cc..cc6612c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,22 +1,23 @@ +--- default_language_version: python: python repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - - id: check-merge-conflict - - id: debug-statements - stages: [commit] - - id: end-of-file-fixer - stages: [commit] - - id: trailing-whitespace -- repo: https://github.com/psf/black + - id: check-merge-conflict + - id: debug-statements + stages: [commit] + - id: end-of-file-fixer + stages: [commit] + - id: trailing-whitespace + - repo: https://github.com/psf/black rev: 25.11.0 hooks: - - id: black - language_version: python3 - exclude: 'mathicsscript/version.py' -- repo: https://github.com/pycqa/flake8 + - id: black + language_version: python3 + exclude: 'pymathics/icu/version.py' + - repo: https://github.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 From 4db8f7a9c6f1e5e37cba8f95ff54491decb8c4b0 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 9 Jun 2026 12:13:20 -0400 Subject: [PATCH 5/7] CI woes --- .github/workflows/autoblack.yml | 20 ++++++------ .github/workflows/isort-and-black-checks.yml | 34 -------------------- 2 files changed, 10 insertions(+), 44 deletions(-) delete mode 100644 .github/workflows/isort-and-black-checks.yml diff --git a/.github/workflows/autoblack.yml b/.github/workflows/autoblack.yml index 7e754a2..f7ba73f 100644 --- a/.github/workflows/autoblack.yml +++ b/.github/workflows/autoblack.yml @@ -1,23 +1,23 @@ -# GitHub Action that uses Black to reformat the Python code in an incoming pull request. -# If all Python code in the pull request is compliant with Black then this Action does nothing. -# Othewrwise, Black is run and its changes are committed back to the incoming pull request. -# https://github.com/cclauss/autoblack +# GitHub Action that uses Black to reformat the Python code in an +# incoming pull request. If all Python code in the pull request is +# compliant with Black then this Action does nothing. Othewrwise, +# Black is run and its changes are committed back to the incoming pull +# request. https://github.com/cclauss/autoblack +--- name: autoblack on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - name: Set up Python 3.13 - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - name: Set up Python 3.14 + uses: actions/setup-python@v6 with: python-version: 3.14 - name: Install click, black and isort - run: pip install 'click==8.2.1' 'black==25.11.0' 'isort==5.13.2' - - name: Run isort --check . - run: isort --check . + run: pip install 'click==8.2.1' 'black==25.11.0' 'isort==8.0.1' - name: Run black --check --diff . run: black --check --diff . - name: If needed, commit black changes to the pull request diff --git a/.github/workflows/isort-and-black-checks.yml b/.github/workflows/isort-and-black-checks.yml deleted file mode 100644 index 00bf1dc..0000000 --- a/.github/workflows/isort-and-black-checks.yml +++ /dev/null @@ -1,34 +0,0 @@ -# GitHub Action that uses Black to reformat the Python code in an -# incoming pull request. If all Python code in the pull request is -# compliant with Black then this Action does nothing. Othewrwise, -# Black is run and its changes are committed back to the incoming pull -# request. https://github.com/cclauss/autoblack - ---- -name: isort and black check -on: [pull_request] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Set up Python 3.14 - uses: actions/setup-python@v6 - with: - python-version: 3.14 - - name: Install click, black and isort - run: pip install 'click==8.2.1' 'black==25.11.0' 'isort==8.0.1' - - name: Run isort --check . - run: isort --check . - - name: Run black --check --diff . - run: black --check --diff . - # - name: If needed, commit black changes to the pull request - # if: failure() - # run: | - # black . - # git config --global user.name 'autoblack' - # git config --global user.email 'rocky@users.noreply.github.com' - # git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY - # git checkout $GITHUB_HEAD_REF - # git commit -am "fixup: Format Python code with Black" - # git push From e3b78fc7c13bf0d655f61accb8b139281075e515 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 9 Jun 2026 12:57:37 -0400 Subject: [PATCH 6/7] Work towards getting error messages under control --- pymathics/icu/__main__.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/pymathics/icu/__main__.py b/pymathics/icu/__main__.py index 299bdb1..a85c502 100644 --- a/pymathics/icu/__main__.py +++ b/pymathics/icu/__main__.py @@ -53,7 +53,9 @@ class AlphabeticOrderOptions: """what language or alphabet to assume""" @classmethod - def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": + def from_dict( + cls, options: dict[str, Any], evaluation: Evaluation + ) -> Optional["AlphabeticOrderOptions"]: """Factory method that normalizes, type-checks, and builds the frozen structure from a raw dict[str, str]. """ @@ -79,7 +81,8 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": normalized_key = key_mapping.get(raw_key) if not normalized_key: - raise TypeError(f"Unknown option field provided: '{raw_key}'") + evaluation.message("AlphabeticOrder", "nodef", Symbol(raw_key), String("AlphabeticOrder")) + return # Type parsing and validation based on the target field name if normalized_key in ( @@ -88,10 +91,8 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": "ignore_punctuation", ): if option_value not in (SymbolTrue, SymbolFalse): - raise TypeError( - f"Field '{raw_key}' expects a Boolean value. " - f"Got: '{option_value}'" - ) + evaluation.message("AlphabeticOrder", "nodef", Symbol(raw_key), String("AlphabeticOrder")) + return processed_args[normalized_key] = option_value.value elif normalized_key == "language": @@ -99,10 +100,8 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": option_value = String(LANGUAGE) if not isinstance(option_value, String): - raise TypeError( - f"Field '{raw_key}' expects a String value. " - f"Got: '{option_value}'" - ) + evaluation.message("AlphabeticOrder", "nodef", Symbol(raw_key), String("AlphabeticOrder")) + return processed_args[normalized_key] = option_value elif normalized_key == "lowercase_ordering": @@ -113,11 +112,8 @@ def from_dict(cls, options: dict[str, Any]) -> "AlphabeticOrderOptions": elif option_value == StringUpperFirst: processed_args[normalized_key] = False else: - breakpoint() - raise TypeError( - f"Field 'CaseOrdering' expects a 'UpperFirst', 'LowerFirst' or Automatic " - f"Got: '{option_value}'" - ) + evaluation.message("AlphabeticOrder", "nodef", Symbol(raw_key), String("AlphabeticOrder")) + return # Initialize and return the frozen dataclass using our verified arguments return cls(**processed_args) @@ -256,7 +252,7 @@ class Alphabet(Builtin): """ messages = { - "nalph": "The alphabet `` is not known or not available.", + "nalph": "The alphabet `1` is not known or not available.", } rules = { @@ -364,7 +360,9 @@ def eval_with_lang( ): """AlphabeticOrder[string1_String, string2_String, lang_String, OptionsPattern[%(name)s]]""" - alphabetic_order_options = AlphabeticOrderOptions.from_dict(options) + alphabetic_order_options = AlphabeticOrderOptions.from_dict(options, evaluation) + if alphabetic_order_options is None: + return return Integer( eval_alphabetic_order( From bf367220bca5c85e9ccd93e3a55ce21abb7d6b13 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 10 Jun 2026 06:13:28 -0400 Subject: [PATCH 7/7] Black --- pymathics/icu/__main__.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/pymathics/icu/__main__.py b/pymathics/icu/__main__.py index a85c502..dfe7500 100644 --- a/pymathics/icu/__main__.py +++ b/pymathics/icu/__main__.py @@ -81,7 +81,12 @@ def from_dict( normalized_key = key_mapping.get(raw_key) if not normalized_key: - evaluation.message("AlphabeticOrder", "nodef", Symbol(raw_key), String("AlphabeticOrder")) + evaluation.message( + "AlphabeticOrder", + "nodef", + Symbol(raw_key), + String("AlphabeticOrder"), + ) return # Type parsing and validation based on the target field name @@ -91,7 +96,12 @@ def from_dict( "ignore_punctuation", ): if option_value not in (SymbolTrue, SymbolFalse): - evaluation.message("AlphabeticOrder", "nodef", Symbol(raw_key), String("AlphabeticOrder")) + evaluation.message( + "AlphabeticOrder", + "nodef", + Symbol(raw_key), + String("AlphabeticOrder"), + ) return processed_args[normalized_key] = option_value.value @@ -100,7 +110,12 @@ def from_dict( option_value = String(LANGUAGE) if not isinstance(option_value, String): - evaluation.message("AlphabeticOrder", "nodef", Symbol(raw_key), String("AlphabeticOrder")) + evaluation.message( + "AlphabeticOrder", + "nodef", + Symbol(raw_key), + String("AlphabeticOrder"), + ) return processed_args[normalized_key] = option_value @@ -112,7 +127,12 @@ def from_dict( elif option_value == StringUpperFirst: processed_args[normalized_key] = False else: - evaluation.message("AlphabeticOrder", "nodef", Symbol(raw_key), String("AlphabeticOrder")) + evaluation.message( + "AlphabeticOrder", + "nodef", + Symbol(raw_key), + String("AlphabeticOrder"), + ) return # Initialize and return the frozen dataclass using our verified arguments