From 76df4bf07ba3ed7a6dfdc7dee1d5bb5993567a0f Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 19 Feb 2026 11:46:38 -0500 Subject: [PATCH 1/7] Revise for changed Mathics-scanner API --- .github/workflows/pyodide.yml | 2 +- Makefile | 10 +++++----- admin-tools/make-JSON-tables.sh | 7 +++---- mathics/builtin/forms/data.py | 2 +- mathics/core/convert/op.py | 2 +- mathics/core/parser/operators.py | 2 +- mathics/core/parser/parser.py | 22 +++++++++++----------- mathics/data/.gitignore | 3 ++- mathics/eval/list/eol.py | 6 +++++- setup.py | 15 ++++++--------- test/builtin/test_forms.py | 2 ++ 11 files changed, 38 insertions(+), 35 deletions(-) diff --git a/.github/workflows/pyodide.yml b/.github/workflows/pyodide.yml index 52b0eba21..8802a1e64 100644 --- a/.github/workflows/pyodide.yml +++ b/.github/workflows/pyodide.yml @@ -58,5 +58,5 @@ jobs: python -m pip install --no-build-isolation -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner pip install --no-build-isolation -e . - make mathics/data/character-tables.json mathics/data/operator-tables.json + bash ./admin-tools/make-JSON-tables.sh make -j3 check diff --git a/Makefile b/Makefile index 1bd9e3e6a..f3241fa85 100644 --- a/Makefile +++ b/Makefile @@ -69,17 +69,17 @@ build: # because pip install doesn't handle # INSTALL_REQUIRES properly #: Set up to run from the source tree -develop: mathics/data/character-tables.json mathics/data/operator-tables.json +develop: mathics_scanner/data/boxing-characters.json mathics_scanner/data/named-characters.json mathics_scanner/data/operators.json $(PIP) install -e .[dev] # See note above on ./setup.py #: Set up to run from the source tree with full dependencies -develop-full: mathics/data/character-tables.json mathics/data/operators.json +develop-full: mathics_scanner/data/boxing-characters.json mathics_scanner/data/named-characters.json mathics_scanner/data/operators.json $(PIP) install -e .[dev,full] # See note above on ./setup.py #: Set up to run from the source tree with full dependencies and Cython -develop-full-cython: mathics/data/character-tables.json mathics/data/operators.json +develop-full-cython: mathics_scanner/data/boxing-characters.json mathics_scanner/data/named-characters.json mathics_scanner/data/operators.json $(PIP) install -e .[dev,full,cython] @@ -126,7 +126,7 @@ clean: clean-cython clean-cache ($(MAKE) -C "$$dir" clean); \ done; \ rm -f factorials || true; \ - rm -f mathics/data/character-tables.json || true; \ + rm -f mathics/data/*.json || true; \ rm -rf build || true mypy: @@ -166,7 +166,7 @@ latexdoc texdoc doc: (cd mathics/doc/latex && $(MAKE) doc) #: Build JSON ASCII to unicode opcode table and operator table -mathics/data/operator-tables.json mathics/data/character-tables.json mathics/data/operators.json: +mathics_scanner/data/boxing-characters.json mathics_scanner/data/named-characters.json mathics_scanner/data/operators.json: $(BASH) ./admin-tools/make-JSON-tables.sh #: Remove ChangeLog diff --git a/admin-tools/make-JSON-tables.sh b/admin-tools/make-JSON-tables.sh index a9d802a8d..c0b82ec16 100755 --- a/admin-tools/make-JSON-tables.sh +++ b/admin-tools/make-JSON-tables.sh @@ -5,7 +5,6 @@ mydir=$(dirname $bs) PYTHON=${PYTHON:-python} cd $mydir/../mathics/data -mathics3-generate-json-table -o character-tables.json -mathics3-generate-operator-json-table -o operator-tables.json -# tokenizer looks for the table in the default place... -mathics3-generate-operator-json-table +mathics3-make-boxing-character-json -o boxing-characters.json +mathics3-make-named-character-json -o named-characters.json +mathics3-make-operator-json -o operators.json diff --git a/mathics/builtin/forms/data.py b/mathics/builtin/forms/data.py index 8b1b3632e..1f00a8f17 100644 --- a/mathics/builtin/forms/data.py +++ b/mathics/builtin/forms/data.py @@ -679,7 +679,7 @@ class StringForm(FormBaseClass): = `` is Global`a To use a 'Backquote' as a character, escape it with a backslash: - >> StringForm["`` is Global\`a", a] + >> StringForm["`` is Global\\`a", a] = a is Global`a Elements are formatted according the enclosing context: diff --git a/mathics/core/convert/op.py b/mathics/core/convert/op.py index 15acf2450..4616b56a2 100644 --- a/mathics/core/convert/op.py +++ b/mathics/core/convert/op.py @@ -15,7 +15,7 @@ # Load the conversion tables from disk -characters_path = osp.join(ROOT_DIR, "data", "character-tables.json") +characters_path = osp.join(ROOT_DIR, "data", "named-characters.json") assert osp.exists( characters_path ), f"ASCII operator to Unicode tables are missing from {characters_path}" diff --git a/mathics/core/parser/operators.py b/mathics/core/parser/operators.py index 6418f5187..ac76b2e94 100644 --- a/mathics/core/parser/operators.py +++ b/mathics/core/parser/operators.py @@ -21,7 +21,7 @@ # Load Mathics3 operator information from JSON. This file is derived from a # Mathics3 Operator Data YAML file in MathicsScanner. -operator_tables_path = osp.join(ROOT_DIR, "data", "operator-tables.json") +operator_tables_path = osp.join(ROOT_DIR, "data", "operators.json") assert osp.exists( operator_tables_path ), f"Internal error: Mathics3 Operator information are missing; expected to be in {operator_tables_path}" diff --git a/mathics/core/parser/parser.py b/mathics/core/parser/parser.py index eff3d3844..2e9e4fe6b 100644 --- a/mathics/core/parser/parser.py +++ b/mathics/core/parser/parser.py @@ -137,7 +137,7 @@ def get_more_input(self, pos: int): self.backtrack(pos) @property - def is_inside_rowbox(self) -> bool: + def is_inside_box_expression(self) -> bool: r""" Return True iff we parsing inside a RowBox, i.e. RowBox[...] or \( ... \) @@ -264,7 +264,7 @@ def parse_box_expr(self, precedence: int) -> Union[String, Node]: result = None new_result = None while True: - if self.is_inside_rowbox: + if self.is_inside_box_expression: token = self.next_noend() else: token = self.next() @@ -430,15 +430,15 @@ def parse_box_escape(self, token: Token, precedence: int) -> Optional[Node]: # Note: Number and String below are the mathics.core.parser's Number, String and Symbol, # not mathics.core.atom's Number and String, and Symbol. - if self.is_inside_rowbox and isinstance(result, Number): + if self.is_inside_box_expression and isinstance(result, Number): tag, pre_error, post_error = self.tokeniser.sntx_message(token.pos) raise InvalidSyntaxError(tag, pre_error, post_error) while True: - if self.bracket_depth > 0 or self.is_inside_rowbox: + if self.bracket_depth > 0 or self.is_inside_box_expression: token = self.next_noend() if token.tag in ("OtherscriptBox", "RightRowBox"): - if self.is_inside_rowbox: + if self.is_inside_box_expression: break else: tag, pre_error, post_error = self.tokeniser.sntx_message( @@ -511,14 +511,14 @@ def parse_expr(self, precedence: int) -> Optional[Node]: # Note: Number and String below are the mathics.core.parser's Number, String and Symbol, # not mathics.core.atom's Number and String, and Symbol. - if self.is_inside_rowbox and isinstance(result, (Number, Symbol)): + if self.is_inside_box_expression and isinstance(result, (Number, Symbol)): result = String(result.value) while True: - if self.bracket_depth > 0 or self.is_inside_rowbox: + if self.bracket_depth > 0 or self.is_inside_box_expression: token = self.next_noend() if token.tag in ("OtherscriptBox", "RightRowBox"): - if self.is_inside_rowbox: + if self.is_inside_box_expression: break else: tag, pre_error, post_error = self.tokeniser.sntx_message( @@ -550,7 +550,7 @@ def parse_expr(self, precedence: int) -> Optional[Node]: tag not in self.halt_tags and flat_binary_operators["Times"] >= precedence ): - if self.is_inside_rowbox: + if self.is_inside_box_expression: if tag in box_operators: new_result = self.parse_box_operator(result, token, precedence) else: @@ -589,7 +589,7 @@ def parse_p(self): operator_precedence = prefix_operators[tag] child = self.parse_expr(operator_precedence) return Node(tag, child) - elif self.is_inside_rowbox: + elif self.is_inside_box_expression: return None else: tag, pre_error, post_error = self.tokeniser.sntx_message(token.pos) @@ -1005,7 +1005,7 @@ def e_RawLeftBracket(self, expr, token: Token, p: int) -> Optional[Node]: self.expect("RawRightBracket") self.bracket_depth -= 1 - if self.is_inside_rowbox: + if self.is_inside_box_expression: # Handle function calls inside a RowBox. result = Node("List", expr, String("["), *seq, String("]")) else: diff --git a/mathics/data/.gitignore b/mathics/data/.gitignore index 9c07e628a..1b2b265e3 100644 --- a/mathics/data/.gitignore +++ b/mathics/data/.gitignore @@ -1,5 +1,6 @@ /.python-version /doc_latex_data.pcl /doctest_latex_data.pcl -/character-tables.json +/boxing-characters.json +/named-characters.json /operator-tables.json diff --git a/mathics/eval/list/eol.py b/mathics/eval/list/eol.py index a20326293..b85fae524 100644 --- a/mathics/eval/list/eol.py +++ b/mathics/eval/list/eol.py @@ -1,3 +1,5 @@ +from typing import List + from mathics.core.atoms import Integer from mathics.core.evaluation import Evaluation from mathics.core.exceptions import MessageException @@ -63,7 +65,9 @@ def select(inner): return select -def eval_Part(list_of_list, indices, evaluation: Evaluation, assign_rhs=None): +def eval_Part( + list_of_list: list, indices: List[Integer], evaluation: Evaluation, assign_rhs=None +): """ eval_part takes the first element of `list_of_list`, and builds a subexpression composed of the expressions at the index positions diff --git a/setup.py b/setup.py index 1bba72e84..f49eb95c1 100644 --- a/setup.py +++ b/setup.py @@ -109,15 +109,12 @@ def get_srcdir(): class build_py(setuptools_build_py): def run(self): - if not os.path.exists("mathics/data/character-tables.json"): - os.system( - "mathics3-generate-json-table" " -o mathics/data/character-tables.json" - ) - if not os.path.exists("mathics/data/operator-tables.json"): - os.system( - "mathics3-generate-operator-json-table" " -o operator-tables.json" - ) - self.distribution.package_data["mathics"].append("data/character-tables.json") + for table_type in ("boxing-character", "named-character", "operator"): + json_data_file = osp.join("data", f"{table_type}.json") + json_path = osp.join("mathics-scanner", json_data_file) + if not osp.exists(json_path): + os.system(f"mathics3-make-{table_type}-json" " -o {json-path}") + self.distribution.package_data["Mathics-Scanner"].append(json_data_file) setuptools_build_py.run(self) diff --git a/test/builtin/test_forms.py b/test/builtin/test_forms.py index 890c988c4..16650ebcb 100644 --- a/test/builtin/test_forms.py +++ b/test/builtin/test_forms.py @@ -419,6 +419,8 @@ def test_output(str_expr, msgs, str_expected, fail_msg): @pytest.mark.parametrize( ("str_expr", "str_expected", "fail_msg", "msgs"), [ + # FIXME: need to decide what to do here. In WMA you get: + # StringForm::sfq: Unmatched backquote in This is symbol \`.. ( 'StringForm["This is symbol ``.", A]', '"This is symbol A."', From 03dd535f72b5b31fc06d2dc72905cc0c68030d8e Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 20 Feb 2026 09:08:21 -0500 Subject: [PATCH 2/7] Go over Characters[] builtin Handle unicode for boxing operators inside strings. --- mathics/builtin/string/characters.py | 6 +++--- mathics/eval/arithfns/basic.py | 2 +- mathics/eval/string/__init__.py | 3 +++ mathics/eval/string/characters.py | 18 ++++++++++++++++++ test/helper.py | 11 +++++------ 5 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 mathics/eval/string/__init__.py create mode 100644 mathics/eval/string/characters.py diff --git a/mathics/builtin/string/characters.py b/mathics/builtin/string/characters.py index a5c86fc68..2e7751405 100644 --- a/mathics/builtin/string/characters.py +++ b/mathics/builtin/string/characters.py @@ -7,9 +7,9 @@ from mathics.core.atoms import String from mathics.core.attributes import A_LISTABLE, A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin, Test -from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation from mathics.core.list import ListExpression +from mathics.eval.string.characters import eval_Characters class Characters(Builtin): @@ -30,10 +30,10 @@ class Characters(Builtin): attributes = A_LISTABLE | A_PROTECTED summary_text = "list the characters in a string" - def eval(self, string, evaluation: Evaluation): + def eval(self, string: String, evaluation: Evaluation) -> ListExpression: "Characters[string_String]" - return to_mathics_list(*string.value, elements_conversion_fn=String) + return eval_Characters(string.value) class CharacterRange(Builtin): diff --git a/mathics/eval/arithfns/basic.py b/mathics/eval/arithfns/basic.py index ab78c09a2..cac108129 100644 --- a/mathics/eval/arithfns/basic.py +++ b/mathics/eval/arithfns/basic.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -evaluation function for builtins in mathics.builtin.arithfns.basic +Evaluation function for builtins in mathics.builtin.arithfns.basic. Many of these depend on the evaluation context. Conversions to SymPy are used just as a last resource. diff --git a/mathics/eval/string/__init__.py b/mathics/eval/string/__init__.py new file mode 100644 index 000000000..2a065b487 --- /dev/null +++ b/mathics/eval/string/__init__.py @@ -0,0 +1,3 @@ +""" +Module tracking eval functions under mathics.builtin.string +""" diff --git a/mathics/eval/string/characters.py b/mathics/eval/string/characters.py new file mode 100644 index 000000000..eacc644f0 --- /dev/null +++ b/mathics/eval/string/characters.py @@ -0,0 +1,18 @@ +""" +Evaluation functions for builtins in mathics.builtin.string. +""" + +from mathics_scanner.characters import replace_box_unicode_with_ascii + +from mathics.core.atoms import String +from mathics.core.convert.expression import to_mathics_list +from mathics.core.list import ListExpression + + +def eval_Characters(string: str) -> ListExpression: + "Characters[string_String]" + + return to_mathics_list( + *(replace_box_unicode_with_ascii(s) for s in string), + elements_conversion_fn=String, + ) diff --git a/test/helper.py b/test/helper.py index 0baa2205c..e03a4e5ad 100644 --- a/test/helper.py +++ b/test/helper.py @@ -9,7 +9,7 @@ import_and_load_builtins() -# Set up a Mathics session with definitions. +# Set up a Mathics3 session with definitions. # For consistency set the character encoding ASCII which is # the lowest common denominator available on all systems. session = MathicsSession(character_encoding="ASCII") @@ -22,17 +22,16 @@ def reset_session(add_builtin=True, catch_interrupt=False): """ reset the session cleaning all the definitions. """ - global session session.reset() session.evaluate("SetDirectory[$TemporaryDirectory];") -def evaluate_value(str_expr: str): - return session.evaluate(str_expr).value +def evaluate_value(str_expr: str, form=None): + return session.evaluate(str_expr, form=form).value -def evaluate(str_expr: str): - return session.evaluate(str_expr) +def evaluate(str_expr: str, form=None): + return session.evaluate(str_expr, form=form) def check_evaluation( From 4225c24ecb6060816f8ba8562df6f60d48a5dd9b Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 20 Feb 2026 10:33:38 -0500 Subject: [PATCH 3/7] Adjust for Boxing ascii operators in strings... and add more tests. Check more argument counts on some builtin functions. --- mathics/builtin/forms/data.py | 2 +- mathics/builtin/string/characters.py | 2 + mathics/builtin/string/charcodes.py | 2 + mathics/builtin/string/operations.py | 2 + mathics/eval/strings.py | 11 +- mathics/format/form/outputform.py | 5 +- test/builtin/string/test_characters.py | 95 +++++++++++++++ test/builtin/string/test_charcodes.py | 148 ++++++++++++++++++++++ test/builtin/strings/__init__.py | 0 test/builtin/strings/test_operations.py | 93 -------------- test/builtin/test_strings.py | 155 +++--------------------- 11 files changed, 281 insertions(+), 234 deletions(-) create mode 100644 test/builtin/string/test_characters.py create mode 100644 test/builtin/string/test_charcodes.py delete mode 100644 test/builtin/strings/__init__.py delete mode 100644 test/builtin/strings/test_operations.py diff --git a/mathics/builtin/forms/data.py b/mathics/builtin/forms/data.py index 1f00a8f17..2fd8510e4 100644 --- a/mathics/builtin/forms/data.py +++ b/mathics/builtin/forms/data.py @@ -699,7 +699,7 @@ class StringForm(FormBaseClass): } summary_text = "format a string from a template and a list of parameters" - def eval_makeboxes(self, s, args, form, evaluation): + def eval_makeboxes(self, s: String, args, form, evaluation: Evaluation): """MakeBoxes[StringForm[s_String, args___], form:StandardForm|TraditionalForm]""" try: diff --git a/mathics/builtin/string/characters.py b/mathics/builtin/string/characters.py index 2e7751405..8612ecf10 100644 --- a/mathics/builtin/string/characters.py +++ b/mathics/builtin/string/characters.py @@ -28,6 +28,8 @@ class Characters(Builtin): """ attributes = A_LISTABLE | A_PROTECTED + eval_error = Builtin.generic_argument_error + expected_args = 1 summary_text = "list the characters in a string" def eval(self, string: String, evaluation: Evaluation) -> ListExpression: diff --git a/mathics/builtin/string/charcodes.py b/mathics/builtin/string/charcodes.py index 22bcfd8b3..a0c8f8ff1 100644 --- a/mathics/builtin/string/charcodes.py +++ b/mathics/builtin/string/charcodes.py @@ -65,6 +65,8 @@ class ToCharacterCode(Builtin): = -Graphics- """ + eval_error = Builtin.generic_argument_error + expected_args = (1, 2) summary_text = "convert a string to a list of character codes" def _encode(self, string, encoding, evaluation: Evaluation): diff --git a/mathics/builtin/string/operations.py b/mathics/builtin/string/operations.py index d52ce55be..3c318af54 100644 --- a/mathics/builtin/string/operations.py +++ b/mathics/builtin/string/operations.py @@ -360,6 +360,8 @@ class StringLength(Builtin): """ attributes = A_LISTABLE | A_PROTECTED + eval_error = Builtin.generic_argument_error + expected_args = 1 summary_text = "length of a string (in Unicode characters)" diff --git a/mathics/eval/strings.py b/mathics/eval/strings.py index ad718dafe..46a652ff4 100644 --- a/mathics/eval/strings.py +++ b/mathics/eval/strings.py @@ -4,6 +4,8 @@ import re +from mathics_scanner.characters import replace_box_unicode_with_ascii + from mathics.builtin.box.layout import RowBox from mathics.core.atoms import Integer, Integer0, Integer1, Integer3, String from mathics.core.convert.expression import to_mathics_list @@ -155,7 +157,7 @@ def safe_backquotes(string: str): return string -def eval_StringForm_MakeBoxes(strform, items, form, evaluation): +def eval_StringForm_MakeBoxes(strform: String, items, form, evaluation: Evaluation): """MakeBoxes[StringForm[s_String, items___], form_]""" if not isinstance(strform, String): @@ -164,10 +166,13 @@ def eval_StringForm_MakeBoxes(strform, items, form, evaluation): items = [format_element(item, evaluation, form) for item in items] curr_indx = 0 - strform_str = safe_backquotes(strform.value) + strform_str = safe_backquotes(replace_box_unicode_with_ascii(strform.value)) parts = strform_str.split("`") - parts = [part.replace("\\[RawBackquote]", "`") for part in parts] + + # Rocky: This looks like a hack to me: is it needed? + parts = [part.replace(r"\[RawBackquote]", "`") for part in parts] + result = [String(parts[0])] if len(parts) <= 1: return result[0] diff --git a/mathics/format/form/outputform.py b/mathics/format/form/outputform.py index 7683328a0..3cb548e36 100644 --- a/mathics/format/form/outputform.py +++ b/mathics/format/form/outputform.py @@ -8,6 +8,8 @@ import re from typing import Callable, Dict, List, Union +from mathics_scanner.characters import replace_box_unicode_with_ascii + from mathics.core.atoms import ( Integer, Integer0, @@ -861,8 +863,9 @@ def stringform_render_output_form( items = [render_output_form(item, evaluation, **kwargs) for item in items] curr_indx = 0 - strform_str = safe_backquotes(strform.value) + strform_str = safe_backquotes(replace_box_unicode_with_ascii(strform.value)) parts = strform_str.split("`") + # Rocky: This looks like a hack to me: is it needed? parts = [part.replace("\\[RawBackquote]", "`") for part in parts] result = [parts[0]] if len(parts) <= 1: diff --git a/test/builtin/string/test_characters.py b/test/builtin/string/test_characters.py new file mode 100644 index 000000000..28e9b276e --- /dev/null +++ b/test/builtin/string/test_characters.py @@ -0,0 +1,95 @@ +""" +Test builtin function Characters[] +""" + +import time +from test.helper import check_evaluation, evaluate +from typing import Optional + +import pytest + +from mathics.core.atoms import String +from mathics.core.list import ListExpression +from mathics.session import MathicsSession + +# Set up a Mathics session with definitions. +# For consistency set the character encoding ASCII which is +# the lowest common denominator available on all systems. +session = MathicsSession(character_encoding="ASCII") + + +def check_characters_evaluation( + str_expr: str, + expected: ListExpression, + failure_message: Optional[str] = "", +): + """ + Helper function to test Mathics expression against + its results. + + Compares the expressions represented by ``str_expr`` and ``str_expected`` by + evaluating the first, and optionally, the second. If omitted, `str_expected` + is assumed to be `"Null"`. + + str_expr: The expression to be tested. If its value is ``None``, the session is + reset. + At the beginning of each set of pytests, it is important to call + ``check_evaluation(None)`` to avoid that definitions introduced by + other tests affect the results. + + str_expected: The expected result. The value ``None`` is equivalent to ``"Null"``. + + failure_message: message shown in case of failure. Use "" for no failure message. + """ + result = evaluate(str_expr) + + print(time.asctime()) + if failure_message: + print(f"got: \n{result}\nexpect:\n{expected}\n -- {failure_message}") + assert result == expected, failure_message + else: + print(f"got: \n{result}\nexpect:\n{expected}\n --") + assert result == expected + + +@pytest.mark.parametrize( + ("str_expr", "msgs", "str_expected", "fail_msg"), + [ + ( + r'Characters["\\\` "]', + None, + ListExpression(String("\\"), String(r"\`"), String(" ")), + "Characters[] with an escape sequence that should be treated as one character", + ), + ], +) +def test_characters_with_escape_sequence(str_expr, msgs, str_expected, fail_msg): + check_characters_evaluation( + str_expr, + str_expected, + failure_message=fail_msg, + ) + + +@pytest.mark.parametrize( + ("str_expr", "msgs", "str_expected", "fail_msg"), + [ + ( + "Characters[]", + ["Characters called with 0 arguments; 1 argument is expected."], + "Characters[]", + "Characters argument checking", + ), + ], +) +def test_characters(str_expr, msgs, str_expected, fail_msg): + """ """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=fail_msg, + expected_messages=msgs, + ) diff --git a/test/builtin/string/test_charcodes.py b/test/builtin/string/test_charcodes.py new file mode 100644 index 000000000..2d815e17d --- /dev/null +++ b/test/builtin/string/test_charcodes.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +""" +Unit tests from mathics.builtin.string. +""" + +from test.helper import check_evaluation + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "msgs", "str_expected", "fail_msg"), + [ + ('ToCharacterCode[{"ab"}]', None, "{{97, 98}}", None), + (r'ToCharacterCode[{"\(A\)"}]', None, "{{63433, 65, 63424}}", None), + ( + 'ToCharacterCode[{{"ab"}}]', + ( + "String or list of strings expected at position 1 in ToCharacterCode[{{ab}}].", + ), + "ToCharacterCode[{{ab}}]", + None, + ), + ( + "ToCharacterCode[x]", + ( + "String or list of strings expected at position 1 in ToCharacterCode[x].", + ), + "ToCharacterCode[x]", + None, + ), + ('ToCharacterCode[""]', None, "{}", None), + ( + "#1 == ToCharacterCode[FromCharacterCode[#1]] & [RandomInteger[{0, 65535}, 100]]", + None, + "True", + None, + ), + ( + r"ToCharacterCode[]", + ["ToCharacterCode called with 0 arguments; 1 or 2 arguments are expected."], + "ToCharacterCode[]", + "ToCharacterCode argument checking", + ), + ], +) +def test_to_character_code(str_expr, msgs, str_expected, fail_msg): + """ """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=fail_msg, + expected_messages=msgs, + ) + + +@pytest.mark.parametrize( + ("str_expr", "msgs", "str_expected", "fail_msg"), + [ + ("FromCharacterCode[{}] // InputForm", None, '""', None), + ( + "FromCharacterCode[65536]", + ( + "A character code, which should be a non-negative integer less than 65536, is expected at position 1 in {65536}.", + ), + "FromCharacterCode[65536]", + None, + ), + ( + "FromCharacterCode[-1]", + ( + "Non-negative machine-sized integer expected at position 1 in FromCharacterCode[-1].", + ), + "FromCharacterCode[-1]", + None, + ), + ( + "FromCharacterCode[444444444444444444444444444444444444]", + ( + "Non-negative machine-sized integer expected at position 1 in FromCharacterCode[444444444444444444444444444444444444].", + ), + "FromCharacterCode[444444444444444444444444444444444444]", + None, + ), + ( + "FromCharacterCode[{100, 101, -1}]", + ( + "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {100, 101, -1}.", + ), + "FromCharacterCode[{100, 101, -1}]", + None, + ), + ( + "FromCharacterCode[{100, 101, 65536}]", + ( + "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {100, 101, 65536}.", + ), + "FromCharacterCode[{100, 101, 65536}]", + None, + ), + ( + "FromCharacterCode[{100, 101, x}]", + ( + "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {100, 101, x}.", + ), + "FromCharacterCode[{100, 101, x}]", + None, + ), + ( + "FromCharacterCode[{100, {101}}]", + ( + "A character code, which should be a non-negative integer less than 65536, is expected at position 2 in {100, {101}}.", + ), + "FromCharacterCode[{100, {101}}]", + None, + ), + ( + "FromCharacterCode[{{97, 98, 99}, {100, 101, x}}]", + ( + "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {100, 101, x}.", + ), + "FromCharacterCode[{{97, 98, 99}, {100, 101, x}}]", + None, + ), + ( + "FromCharacterCode[{{97, 98, x}, {100, 101, x}}]", + ( + "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {97, 98, x}.", + ), + "FromCharacterCode[{{97, 98, x}, {100, 101, x}}]", + None, + ), + ], +) +def test_from_character_code(str_expr, msgs, str_expected, fail_msg): + """ """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=fail_msg, + expected_messages=msgs, + ) diff --git a/test/builtin/strings/__init__.py b/test/builtin/strings/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/builtin/strings/test_operations.py b/test/builtin/strings/test_operations.py deleted file mode 100644 index 206976671..000000000 --- a/test/builtin/strings/test_operations.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Test Operations on Strings -""" - -import re -from test.helper import check_evaluation - -import pytest - - -@pytest.mark.parametrize( - ("str_expr", "msgs", "str_expected", "fail_msg"), - [ - ( - 'StringReplace["abcabc", "a" -> "b", -1]', - ( - re.compile( - "Non-negative integer or Infinity expected at position 3 in StringReplace\\[abcabc, a (->)|[→] b, -1\\]" - ), - ), - re.compile("StringReplace\\[abcabc, a (->)|[→] b, -1\\]"), - None, - ), - ('StringReplace["abc", "b" -> 4]', ("String expected.",), "a <> 4 <> c", None), - ('StringReplace["01101100010", "01" .. -> "x"]', None, "x1x100x0", None), - ('StringReplace["abc abcb abdc", "ab" ~~ _ -> "X"]', None, "X Xb Xc", None), - ( - 'StringReplace["abc abcd abcd", WordBoundary ~~ "abc" ~~ WordBoundary -> "XX"]', - None, - "XX abcd abcd", - None, - ), - ( - 'StringReplace["abcd acbd", RegularExpression["[ab]"] -> "XX"]', - None, - "XXXXcd XXcXXd", - None, - ), - ( - 'StringReplace["abcd acbd", RegularExpression["[ab]"] ~~ _ -> "YY"]', - None, - "YYcd YYYY", - None, - ), - ( - 'StringReplace["abcdabcdaabcabcd", {"abc" -> "Y", "d" -> "XXX"}]', - None, - "YXXXYXXXaYYXXX", - None, - ), - ( - 'StringReplace[" Have a nice day. ", (StartOfString ~~ Whitespace) | (Whitespace ~~ EndOfString) -> ""] // FullForm', - None, - '"Have a nice day."', - None, - ), - ('StringReplace["xyXY", "xy" -> "01"]', None, "01XY", None), - ('StringReplace["xyXY", "xy" -> "01", IgnoreCase -> True]', None, "0101", None), - ('StringReplace["abcabc", "a" -> "b", Infinity]', None, "bbcbbc", None), - ( - 'StringReplace[x, "a" -> "b"]', - ( - re.compile( - "String or list of strings expected at position 1 in StringReplace\\[x, a (->)|[→] b\\]." - ), - ), - re.compile("StringReplace\\[x, a (->)|[→] b"), - None, - ), - ( - 'StringReplace["xyzwxyzwaxyzxyzw", x]', - ("x is not a valid string replacement rule.",), - "StringReplace[xyzwxyzwaxyzxyzw, x]", - None, - ), - ( - 'StringReplace["xyzwxyzwaxyzxyzw", x -> y]', - ("Element x is not a valid string or pattern element in x.",), - re.compile("StringReplace\\[xyzwxyzwaxyzxyzw, x (->)|[→] y"), - None, - ), - ], -) -def test_string_replace_errors(str_expr, msgs, str_expected, fail_msg): - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=None, - hold_expected=True, - failure_message=fail_msg, - expected_messages=msgs, - ) diff --git a/test/builtin/test_strings.py b/test/builtin/test_strings.py index 59f75b444..bb2438cfb 100644 --- a/test/builtin/test_strings.py +++ b/test/builtin/test_strings.py @@ -240,9 +240,25 @@ "StringTake[kkkl, -Graphics-]", None, ), + # These tests are commented out due to the bug reported in issue #906 + # Octal and hexadecimal notation works alone, but fails + # as a part of another expression. For example, + # F[\.78\.79\.7A] or "\.78\.79\.7A" produces a syntax error in Mathics. + # Here, this is put inside a ToString[...] and hence, it does not work. + # (r"\.78\.79\.7A=37; xyz", None, '37', "Octal characters. check me."), + # (r"\:0078\:0079\:007A=38;xyz", None, '38', "Hexadecimal characters. Check me."), + # (r"\101\102\103\061\062\063=39;ABC123", None, "39", None), + (r"xyz=.;ABC123=.;\[Alpha]\[Beta]\[Gamma]", None, "\u03b1\u03b2\u03b3", None), + ('LetterQ[""]', None, "True", None), + ( + 'LetterQ["\\[Alpha]\\[Beta]\\[Gamma]\\[Delta]\\[Epsilon]\\[Zeta]\\[Eta]\\[Theta]"]', + None, + "True", + None, + ), ], ) -def test_private_doctests_operations(str_expr, msgs, str_expected, fail_msg): +def test_operations(str_expr, msgs, str_expected, fail_msg): """ """ check_evaluation( str_expr, @@ -353,138 +369,7 @@ def test_private_doctests_operations(str_expr, msgs, str_expected, fail_msg): ('StringMatchQ["ae", "a@e"]', None, "False", None), ], ) -def test_private_doctests_patterns(str_expr, msgs, str_expected, fail_msg): - """ """ - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=fail_msg, - expected_messages=msgs, - ) - - -@pytest.mark.parametrize( - ("str_expr", "msgs", "str_expected", "fail_msg"), - [ - ('ToCharacterCode[{"ab"}]', None, "{{97, 98}}", None), - ( - 'ToCharacterCode[{{"ab"}}]', - ( - "String or list of strings expected at position 1 in ToCharacterCode[{{ab}}].", - ), - "ToCharacterCode[{{ab}}]", - None, - ), - ( - "ToCharacterCode[x]", - ( - "String or list of strings expected at position 1 in ToCharacterCode[x].", - ), - "ToCharacterCode[x]", - None, - ), - ('ToCharacterCode[""]', None, "{}", None), - ( - "#1 == ToCharacterCode[FromCharacterCode[#1]] & [RandomInteger[{0, 65535}, 100]]", - None, - "True", - None, - ), - ("FromCharacterCode[{}] // InputForm", None, '""', None), - ( - "FromCharacterCode[65536]", - ( - "A character code, which should be a non-negative integer less than 65536, is expected at position 1 in {65536}.", - ), - "FromCharacterCode[65536]", - None, - ), - ( - "FromCharacterCode[-1]", - ( - "Non-negative machine-sized integer expected at position 1 in FromCharacterCode[-1].", - ), - "FromCharacterCode[-1]", - None, - ), - ( - "FromCharacterCode[444444444444444444444444444444444444]", - ( - "Non-negative machine-sized integer expected at position 1 in FromCharacterCode[444444444444444444444444444444444444].", - ), - "FromCharacterCode[444444444444444444444444444444444444]", - None, - ), - ( - "FromCharacterCode[{100, 101, -1}]", - ( - "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {100, 101, -1}.", - ), - "FromCharacterCode[{100, 101, -1}]", - None, - ), - ( - "FromCharacterCode[{100, 101, 65536}]", - ( - "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {100, 101, 65536}.", - ), - "FromCharacterCode[{100, 101, 65536}]", - None, - ), - ( - "FromCharacterCode[{100, 101, x}]", - ( - "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {100, 101, x}.", - ), - "FromCharacterCode[{100, 101, x}]", - None, - ), - ( - "FromCharacterCode[{100, {101}}]", - ( - "A character code, which should be a non-negative integer less than 65536, is expected at position 2 in {100, {101}}.", - ), - "FromCharacterCode[{100, {101}}]", - None, - ), - ( - "FromCharacterCode[{{97, 98, 99}, {100, 101, x}}]", - ( - "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {100, 101, x}.", - ), - "FromCharacterCode[{{97, 98, 99}, {100, 101, x}}]", - None, - ), - ( - "FromCharacterCode[{{97, 98, x}, {100, 101, x}}]", - ( - "A character code, which should be a non-negative integer less than 65536, is expected at position 3 in {97, 98, x}.", - ), - "FromCharacterCode[{{97, 98, x}, {100, 101, x}}]", - None, - ), - # These tests are commented out due to the bug reported in issue #906 - # Octal and hexadecimal notation works alone, but fails - # as a part of another expression. For example, - # F[\.78\.79\.7A] or "\.78\.79\.7A" produces a syntax error in Mathics. - # Here, this is put inside a ToString[...] and hence, it does not work. - # (r"\.78\.79\.7A=37; xyz", None, '37', "Octal characters. check me."), - # (r"\:0078\:0079\:007A=38;xyz", None, '38', "Hexadecimal characters. Check me."), - # (r"\101\102\103\061\062\063=39;ABC123", None, "39", None), - (r"xyz=.;ABC123=.;\[Alpha]\[Beta]\[Gamma]", None, "\u03b1\u03b2\u03b3", None), - ('LetterQ[""]', None, "True", None), - ( - 'LetterQ["\\[Alpha]\\[Beta]\\[Gamma]\\[Delta]\\[Epsilon]\\[Zeta]\\[Eta]\\[Theta]"]', - None, - "True", - None, - ), - ], -) -def test_private_doctests_characters(str_expr, msgs, str_expected, fail_msg): +def test_patterns(str_expr, msgs, str_expected, fail_msg): """ """ check_evaluation( str_expr, @@ -498,8 +383,6 @@ def test_private_doctests_characters(str_expr, msgs, str_expected, fail_msg): # These tests are separated due to the bug reported in issue #906 - - @pytest.mark.parametrize( ("str_expr", "str_expected", "fail_msg"), [ @@ -512,7 +395,7 @@ def test_private_doctests_characters(str_expr, msgs, str_expected, fail_msg): ), ], ) -def test_private_doctests_characters2(str_expr, str_expected, fail_msg): +def test_characters2(str_expr, str_expected, fail_msg): """ """ check_evaluation( str_expr, From b3b8ecf49d770f8feedff8b839498fdd1a28a713 Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 20 Feb 2026 11:29:17 -0500 Subject: [PATCH 4/7] Add revised StringTake[] tests... * Add argument error-check test. * Add test to ensure an escape character is treated like one character. --- mathics/builtin/string/operations.py | 4 +- test/builtin/string/test_operations.py | 58 ++++++++++++++++++++++++++ test/builtin/test_strings.py | 21 ---------- 3 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 test/builtin/string/test_operations.py diff --git a/mathics/builtin/string/operations.py b/mathics/builtin/string/operations.py index 3c318af54..9698fe493 100644 --- a/mathics/builtin/string/operations.py +++ b/mathics/builtin/string/operations.py @@ -865,6 +865,8 @@ class StringTake(Builtin): "take": 'Cannot take positions `1` through `2` in "`3`".', } + eval_error = Builtin.generic_argument_error + expected_args = 2 summary_text = "sub-string from a range of positions" def eval(self, string: String, seqspec, evaluation: Evaluation): @@ -896,7 +898,7 @@ def eval(self, string: String, seqspec, evaluation: Evaluation): return String(result[py_slice]) - def eval_strings(self, strings, spec, evaluation): + def eval_strings(self, strings, spec, evaluation: Evaluation): "StringTake[strings__, spec_]" result_list = [] for string in strings.elements: diff --git a/test/builtin/string/test_operations.py b/test/builtin/string/test_operations.py new file mode 100644 index 000000000..45dafbc10 --- /dev/null +++ b/test/builtin/string/test_operations.py @@ -0,0 +1,58 @@ +""" +Test Built-in function in mathics.builtin.string.operations +""" + +from test.helper import check_evaluation + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "msgs", "str_expected", "fail_msg"), + [ + ('StringTake["abcd", 0] // InputForm', None, '""', None), + ('StringTake["abcd", {3, 2}] // InputForm', None, '""', None), + ('StringTake["", {1, 0}] // InputForm', None, '""', None), + ( + 'StringTake["abc", {0, 0}]', + ('Cannot take positions 0 through 0 in "abc".',), + "StringTake[abc, {0, 0}]", + None, + ), + ( + r'StringTake["\!A",{2}]', + None, + "A", + r"Escaped character \! should be treated like a single character", + ), + ( + "StringTake[{2, 4},2]", + ("String or list of strings expected at position 1.",), + "StringTake[{2, 4}, 2]", + None, + ), + ( + 'StringTake["kkkl",Graphics[{}]]', + ("Integer or a list of sequence specifications expected at position 2.",), + "StringTake[kkkl, -Graphics-]", + None, + ), + ( + r"StringTake[]", + ["StringTake called with 0 arguments; 2 arguments are expected."], + "StringTake[]", + "StringTake argument checking", + ), + ], +) +def test_string_take(str_expr, msgs, str_expected, fail_msg): + """ """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=fail_msg, + expected_messages=msgs, + ) diff --git a/test/builtin/test_strings.py b/test/builtin/test_strings.py index bb2438cfb..dc489fba1 100644 --- a/test/builtin/test_strings.py +++ b/test/builtin/test_strings.py @@ -219,27 +219,6 @@ "StringSplit[x, x]", None, ), - ('StringTake["abcd", 0] // InputForm', None, '""', None), - ('StringTake["abcd", {3, 2}] // InputForm', None, '""', None), - ('StringTake["", {1, 0}] // InputForm', None, '""', None), - ( - 'StringTake["abc", {0, 0}]', - ('Cannot take positions 0 through 0 in "abc".',), - "StringTake[abc, {0, 0}]", - None, - ), - ( - "StringTake[{2, 4},2]", - ("String or list of strings expected at position 1.",), - "StringTake[{2, 4}, 2]", - None, - ), - ( - 'StringTake["kkkl",Graphics[{}]]', - ("Integer or a list of sequence specifications expected at position 2.",), - "StringTake[kkkl, -Graphics-]", - None, - ), # These tests are commented out due to the bug reported in issue #906 # Octal and hexadecimal notation works alone, but fails # as a part of another expression. For example, From bc42ee9ae3f21883c115995824c3aec74acd7435 Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 20 Feb 2026 18:30:09 -0500 Subject: [PATCH 5/7] Try to get CI working... --- .github/workflows/macos.yml | 12 +++++++++--- .github/workflows/packages.yml | 8 ++++++-- .github/workflows/plot-tests.yml | 6 +++++- .github/workflows/pyodide.yml | 18 ++++++++++++++---- .github/workflows/ubuntu-bench.yml | 10 ++++++++-- .github/workflows/ubuntu-cython.yml | 6 ++++-- .github/workflows/ubuntu.yml | 9 ++++----- .github/workflows/windows.yml | 11 ++++++++--- 8 files changed, 58 insertions(+), 22 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d0b486530..60abb4238 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -26,11 +26,17 @@ jobs: run: | brew install llvm@14 tesseract remake python -m pip install --upgrade pip - - name: Install Mathics3 with full Python dependencies + - name: Install Mathics3 dependencies run: | # We can comment out after next Mathics-Scanner release - # python -m pip install Mathics-Scanner[full] - python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" + # python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git + cd mathics-scanner/ + pip install -e . + bash -x admin-tools/make-JSON-tables.sh + cd .. + - name: Install Mathics3 + run: | pip install -e . remake -x develop-full - name: Test Mathics3 diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index ff8f437b9..196d73224 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -21,11 +21,15 @@ jobs: - name: Install OS dependencies run: | sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev tesseract-ocr remake - - name: Install Mathics3 with full dependencies + - name: Install dependent Mathics3 programs run: | python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release - python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" + git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git + cd mathics-scanner/ + pip install -e . + bash -x admin-tools/make-JSON-tables.sh + cd .. - name: Run Mathics3 Combinatorica tests run: | git submodule init diff --git a/.github/workflows/plot-tests.yml b/.github/workflows/plot-tests.yml index 7c93fece9..49d2df9dd 100644 --- a/.github/workflows/plot-tests.yml +++ b/.github/workflows/plot-tests.yml @@ -21,7 +21,7 @@ jobs: - name: Install OS dependencies run: | sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev tesseract-ocr remake - - name: Install Mathics3 with full dependencies + - name: Install dependent Mathics3 programs run: | python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release @@ -29,11 +29,15 @@ jobs: git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git cd mathics-scanner/ pip install -e . + bash -x ./admin-tools/make-JSON-tables.sh cd .. # We can comment out after next Mathics-Scanner release # python -m pip install Mathics-Scanner[full] # python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + - name: Install Mathics3 with full dependencies + run: | pip install -e . + bash -x ./admin-tools/make-JSON-tables.sh remake -x develop-full - name: Test Mathics run: | diff --git a/.github/workflows/pyodide.yml b/.github/workflows/pyodide.yml index 8802a1e64..ab9fee23c 100644 --- a/.github/workflows/pyodide.yml +++ b/.github/workflows/pyodide.yml @@ -36,7 +36,9 @@ jobs: actions-cache-folder: emsdk-cache - name: Install pyodide-build - run: pip install pyodide-build + run: | + pip install -U setuptools + pip install pyodide-build - name: Set up Node.js uses: actions/setup-node@v4 @@ -54,9 +56,17 @@ jobs: pip install "setuptools>=70.0.0" PyYAML click packaging pytest + - name: Install dependent Mathics3 programs + run: | # We can comment out after next Mathics-Scanner release - python -m pip install --no-build-isolation -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner - + git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git + cd mathics-scanner/ + pip install -e . + bash -x admin-tools/make-JSON-tables.sh + cd .. + - name: Install Mathics3 + run: | pip install --no-build-isolation -e . - bash ./admin-tools/make-JSON-tables.sh + bash -x admin-tools/make-JSON-tables.sh + python -m pip install pytest make -j3 check diff --git a/.github/workflows/ubuntu-bench.yml b/.github/workflows/ubuntu-bench.yml index 631f674be..576ae2ecc 100644 --- a/.github/workflows/ubuntu-bench.yml +++ b/.github/workflows/ubuntu-bench.yml @@ -21,12 +21,18 @@ jobs: - name: Install OS dependencies run: | sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev tesseract-ocr remake - - name: Install Mathics3 with full dependencies + - name: Install dependent Mathics3 programs run: | python -m pip install --upgrade pip python -m pip install pytest-benchmark # We can comment out after next Mathics-Scanner release - python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" + git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git + cd mathics-scanner/ + pip install -e . + bash -x admin-tools/make-JSON-tables.sh + cd .. + - name: Install Mathics3 with full dependencies + run: | # python -m pip install Mathics-Scanner[full] pip install -e . remake -x develop diff --git a/.github/workflows/ubuntu-cython.yml b/.github/workflows/ubuntu-cython.yml index e704774e4..55298eab7 100644 --- a/.github/workflows/ubuntu-cython.yml +++ b/.github/workflows/ubuntu-cython.yml @@ -20,13 +20,15 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install Mathics3 dependencies run: | sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev tesseract-ocr python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release - python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" + git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git + cd mathics-scanner/ pip install -e . + bash -x admin-tools/make-JSON-tables.sh cd .. - name: Install Mathics with full dependencies diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index f5edc3237..90f57134d 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -21,19 +21,18 @@ jobs: - name: Install OS dependencies run: | sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev tesseract-ocr remake - - name: Install Mathics3 with full dependencies + - name: Install Mathics3 dependencies run: | python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release # python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git - # git clone --single-branch --branch operator-refactor-part1.5 https://github.com/Mathics3/mathics-scanner.git cd mathics-scanner/ pip install -e . + bash -x admin-tools/make-JSON-tables.sh cd .. - # We can comment out after next Mathics-Scanner release - # python -m pip install Mathics-Scanner[full] - # python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + - name: Install Mathics3 + run: | pip install -e . remake -x develop-full - name: Test Mathics diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 425650a5a..3085676d9 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -33,11 +33,17 @@ jobs: set LLVM_DIR="C:\Program Files\LLVM" - name: Install Mathics3 with Python dependencies run: | + pip install pyocr # from full # We can comment out after next Mathics-Scanner release python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" pip install -e . - - # python -m pip install Mathics-Scanner[full] + git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git + cd mathics-scanner/ + pip install -e . + bash -x admin-tools/make-JSON-tables.sh + cd .. + - name: Install Mathics with full dependencies + run: | make develop-full - name: Test Mathics3 # Limit pip install to a basic install *without* full dependencies. @@ -48,7 +54,6 @@ jobs: # we needs some CI that tests running when packages aren't available # So "dev" only below, not "dev,full". run: | - pip install pyocr # from full pip install -e .[dev] make pytest gstest make doctest DOCTEST_OPTIONS="--exclude WordCloud" From 28f1ff497267b523141586a28f42e972d92e2852 Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 22 Feb 2026 06:52:05 -0500 Subject: [PATCH 6/7] Reinstate string tests that now seem to work --- test/builtin/test_strings.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/builtin/test_strings.py b/test/builtin/test_strings.py index dc489fba1..f466e6670 100644 --- a/test/builtin/test_strings.py +++ b/test/builtin/test_strings.py @@ -219,14 +219,9 @@ "StringSplit[x, x]", None, ), - # These tests are commented out due to the bug reported in issue #906 - # Octal and hexadecimal notation works alone, but fails - # as a part of another expression. For example, - # F[\.78\.79\.7A] or "\.78\.79\.7A" produces a syntax error in Mathics. - # Here, this is put inside a ToString[...] and hence, it does not work. - # (r"\.78\.79\.7A=37; xyz", None, '37', "Octal characters. check me."), - # (r"\:0078\:0079\:007A=38;xyz", None, '38', "Hexadecimal characters. Check me."), - # (r"\101\102\103\061\062\063=39;ABC123", None, "39", None), + (r"\.78\.79\.7A=37; xyz", None, "37", "Octal characters. check me."), + (r"\:0078\:0079\:007A=38;xyz", None, "38", "Hexadecimal characters. Check me."), + (r"\101\102\103\061\062\063=39;ABC123", None, "39", None), (r"xyz=.;ABC123=.;\[Alpha]\[Beta]\[Gamma]", None, "\u03b1\u03b2\u03b3", None), ('LetterQ[""]', None, "True", None), ( From 9f9ca469659d8048b7ca605563ee81108491d4a6 Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 22 Feb 2026 07:02:26 -0500 Subject: [PATCH 7/7] Note why we have a RawBackQuote hack in ToString & remove unneeded duplicate code in eval_StringForm_MakeBoxes --- mathics/eval/strings.py | 4 ---- mathics/format/form/outputform.py | 9 ++++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mathics/eval/strings.py b/mathics/eval/strings.py index 46a652ff4..4e83e2848 100644 --- a/mathics/eval/strings.py +++ b/mathics/eval/strings.py @@ -169,10 +169,6 @@ def eval_StringForm_MakeBoxes(strform: String, items, form, evaluation: Evaluati strform_str = safe_backquotes(replace_box_unicode_with_ascii(strform.value)) parts = strform_str.split("`") - - # Rocky: This looks like a hack to me: is it needed? - parts = [part.replace(r"\[RawBackquote]", "`") for part in parts] - result = [String(parts[0])] if len(parts) <= 1: return result[0] diff --git a/mathics/format/form/outputform.py b/mathics/format/form/outputform.py index 3cb548e36..a2e118459 100644 --- a/mathics/format/form/outputform.py +++ b/mathics/format/form/outputform.py @@ -865,8 +865,15 @@ def stringform_render_output_form( curr_indx = 0 strform_str = safe_backquotes(replace_box_unicode_with_ascii(strform.value)) parts = strform_str.split("`") - # Rocky: This looks like a hack to me: is it needed? + + # Rocky: This looks like a hack to me. It is used right now + # to use allow 'Backquote' to be to escaped with a backslash: + # >> StringForm["`` is Global\\`a", a] + # = a is Global`a + # There is probably needs to be another change to the scanner, and/or + # there is something deeper going on and a change to StringForm. parts = [part.replace("\\[RawBackquote]", "`") for part in parts] + result = [parts[0]] if len(parts) <= 1: return result[0]