From 05e2f088ae6600d5a8fdea1411416f461fff24c8 Mon Sep 17 00:00:00 2001 From: David A Roberts Date: Wed, 30 Oct 2024 03:24:49 +1000 Subject: [PATCH] Fix remaining mypy errors in mathics.core (#1147) This fixes the remaining mypy errors in `mathics.core`, with the exception of `mathics.core.expression` which needs a bit more work. --- mathics/core/assignment.py | 3 +- mathics/core/atoms.py | 2 +- mathics/core/builtin.py | 31 ++++++-- mathics/core/convert/expression.py | 6 +- mathics/core/convert/function.py | 4 +- mathics/core/convert/mpmath.py | 2 +- mathics/core/convert/op.py | 2 +- mathics/core/convert/regex.py | 2 +- mathics/core/convert/sympy.py | 50 ++++++++---- mathics/core/definitions.py | 122 +++++++++++++++-------------- mathics/core/element.py | 20 +++-- mathics/core/evaluation.py | 10 ++- mathics/core/formatter.py | 5 +- mathics/core/list.py | 1 + mathics/core/load_builtin.py | 13 ++- mathics/core/number.py | 5 +- mathics/core/parser/convert.py | 9 +-- mathics/core/parser/parser.py | 15 ++-- mathics/core/pattern.py | 86 +++++++++++--------- mathics/core/rules.py | 4 +- mathics/core/streams.py | 9 +-- mathics/core/subexpression.py | 8 -- mathics/core/symbols.py | 5 +- mathics/core/util.py | 6 +- 24 files changed, 247 insertions(+), 173 deletions(-) diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index a4f02287a..84d1baa6c 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -592,7 +592,8 @@ def eval_assign_numericq(self, lhs, rhs, evaluation: Evaluation, tags, upset): if isinstance(target, Symbol): name = target.get_name() definition = evaluation.definitions.get_definition(name) - definition.is_numeric = rhs is SymbolTrue + if definition is not None: + definition.is_numeric = rhs is SymbolTrue return True else: evaluation.message("NumericQ", "set", lhs, rhs) diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index ad98756bc..979593e99 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -6,7 +6,7 @@ import re from typing import Any, Dict, Generic, Optional, Tuple, TypeVar, Union -import mpmath +import mpmath # type: ignore[import-untyped] import sympy from mathics.core.element import BoxElementMixin, ImmutableValueMixin diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index 3b2808293..31d5ad352 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -13,9 +13,20 @@ from abc import ABC from functools import total_ordering from itertools import chain -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, cast +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, + Union, + cast, +) -import mpmath +import mpmath # type: ignore[import-untyped] import pkg_resources import sympy @@ -84,7 +95,7 @@ try: import ujson except ImportError: - import json as ujson + import json as ujson # type: ignore[no-redef] ROOT_DIR = pkg_resources.resource_filename("mathics", "") @@ -551,6 +562,12 @@ def get_sympy_names(self) -> List[str]: return [self.sympy_name] return [] + def to_sympy(self, expr=None, **kwargs): + raise NotImplementedError + + def from_sympy(self, elements: Tuple[BaseElement, ...]) -> Expression: + raise NotImplementedError + # This has to come before MPMathFunction class SympyFunction(SympyObject): @@ -651,6 +668,8 @@ def eval(self, z, evaluation: Evaluation): if not all(isinstance(arg, Number) for arg in args): return + # mypy isn't yet smart enough to recognise that we can only reach this point if all args are Numbers + args = cast(Sequence[Number], args) mpmath_function = self.get_mpmath_function(tuple(args)) if mpmath_function is None: @@ -663,7 +682,9 @@ def eval(self, z, evaluation: Evaluation): d = dps(prec) args = tuple([arg.round(d) for arg in args]) - return eval_mpmath_function(mpmath_function, *args, prec=prec) + return eval_mpmath_function( + mpmath_function, *cast(Sequence[Number], args), prec=prec + ) class MPMathMultiFunction(MPMathFunction): @@ -1235,7 +1256,7 @@ def get_match_candidates( ) -> Tuple[BaseElement]: return elements - def get_match_count(self, vars_dict: dict = {}): + def get_match_count(self, vars_dict: Optional[dict] = None): return (1, 1) def get_sort_key(self, pattern_sort=False) -> tuple: diff --git a/mathics/core/convert/expression.py b/mathics/core/convert/expression.py index 5d402ddc3..b8acc4e31 100644 --- a/mathics/core/convert/expression.py +++ b/mathics/core/convert/expression.py @@ -83,7 +83,7 @@ def to_mathics_list( return list_expression -def to_numeric_args(mathics_args: Type[BaseElement], evaluation) -> list: +def to_numeric_args(mathics_args: BaseElement, evaluation) -> tuple: """ Convert Mathics arguments, such as the arguments in an evaluation method a Python list that is suitable for feeding as arguments @@ -91,8 +91,8 @@ def to_numeric_args(mathics_args: Type[BaseElement], evaluation) -> list: We make use of fast conversions for literals. """ - return ( - tuple(mathics_args.value) + return tuple( + mathics_args.value # type: ignore[attr-defined] if mathics_args.is_literal else numerify(mathics_args, evaluation).get_sequence() ) diff --git a/mathics/core/convert/function.py b/mathics/core/convert/function.py index 75f632465..0250b04c6 100644 --- a/mathics/core/convert/function.py +++ b/mathics/core/convert/function.py @@ -83,7 +83,9 @@ def _pythonized_mathics_expr(*x): def expression_to_callable_and_args( - expr: Expression, vars: list = None, evaluation: Optional[Evaluation] = None + expr: Expression, + vars: Optional[list] = None, + evaluation: Optional[Evaluation] = None, ) -> Tuple[Optional[Callable], Optional[list]]: """ Return a tuple of Python callable and a list of CompileArgs. diff --git a/mathics/core/convert/mpmath.py b/mathics/core/convert/mpmath.py index cf4895667..a33106488 100644 --- a/mathics/core/convert/mpmath.py +++ b/mathics/core/convert/mpmath.py @@ -3,7 +3,7 @@ from functools import lru_cache from typing import Optional, Union -import mpmath +import mpmath # type: ignore[import-untyped] import sympy from mathics.core.atoms import Complex, MachineReal, MachineReal0, PrecisionReal diff --git a/mathics/core/convert/op.py b/mathics/core/convert/op.py index 3a77e4cba..9befa33c4 100644 --- a/mathics/core/convert/op.py +++ b/mathics/core/convert/op.py @@ -10,7 +10,7 @@ try: import ujson except ImportError: - import json as ujson + import json as ujson # type: ignore[no-redef] ROOT_DIR = pkg_resources.resource_filename("mathics", "") diff --git a/mathics/core/convert/regex.py b/mathics/core/convert/regex.py index 45e302d11..e8b67b166 100644 --- a/mathics/core/convert/regex.py +++ b/mathics/core/convert/regex.py @@ -96,7 +96,7 @@ def to_regex_internal( (None, "") is returned if there is an error of some sort. """ - def recurse(x: Expression, quantifiers=q) -> Tuple[Optional[str], str]: + def recurse(x: Expression, quantifiers=q) -> Optional[str]: """ Shortened way to call to_regexp_internal - only the expr and quantifiers change here. diff --git a/mathics/core/convert/sympy.py b/mathics/core/convert/sympy.py index 7af43162f..743d4621e 100644 --- a/mathics/core/convert/sympy.py +++ b/mathics/core/convert/sympy.py @@ -4,7 +4,7 @@ Converts expressions from SymPy to Mathics expressions. Conversion to SymPy is handled directly in BaseElement descendants. """ -from typing import Optional, Type, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, Union, cast import sympy from sympy import Symbol as Sympy_Symbol, false as SympyFalse, true as SympyTrue @@ -66,6 +66,9 @@ SymbolUnequal, ) +if TYPE_CHECKING: + from mathics.core.builtin import SympyObject + BasicSympy = sympy.Expr @@ -74,8 +77,8 @@ SymbolRootSum = Symbol("RootSum") -mathics_to_sympy = {} # here we have: name -> sympy object -sympy_to_mathics = {} +mathics_to_sympy: Dict[str, "SympyObject"] = {} # here we have: name -> sympy object +sympy_to_mathics: Dict[str, "SympyObject"] = {} sympy_singleton_to_mathics = { @@ -104,14 +107,14 @@ } -def is_Cn_expr(name) -> bool: +def is_Cn_expr(name: str) -> bool: """Check if name is of the form {prefix}Cnnn""" if name.startswith(sympy_symbol_prefix) or name.startswith(sympy_slot_prefix): return False if not name.startswith("C"): return False number = name[1:] - return number and number.isdigit() + return number != "" and number.isdigit() def to_sympy_matrix(data, **kwargs) -> Optional[sympy.MutableDenseMatrix]: @@ -131,6 +134,7 @@ class SympyExpression(BasicSympy): is_Function = True nargs = None + expr: Expression def __new__(cls, *exprs): # sympy simplify may also recreate the object if simplification occurred @@ -167,7 +171,11 @@ def __new__(cls, *args): def has_any_symbols(self, *syms) -> bool: """Check if any of the symbols in syms appears in the expression.""" - result = any(arg.has_any_symbols(*syms) for arg in self.args) + result = any( + arg.has_any_symbols(*syms) + for arg in self.args + if isinstance(arg, SympyExpression) + ) return result def _eval_subs(self, old, new): @@ -185,10 +193,14 @@ def _eval_rewrite(self, rule, args, **hints): return self @property - def is_commutative(self) -> bool: + def is_commutative(self) -> Optional[bool]: """Check if the arguments are commutative.""" return all(getattr(t, "is_commutative", False) for t in self.args) + @is_commutative.setter + def is_commutative(self, value: bool) -> None: + return + def __str__(self) -> str: return f"{super().__str__()}[{self.expr}])" @@ -257,7 +269,7 @@ def symbol_to_sympy(symbol: Symbol, **kwargs) -> Sympy_Symbol: return builtin.to_sympy(symbol, **kwargs) -def to_numeric_sympy_args(mathics_args: Type[BaseElement], evaluation) -> list: +def to_numeric_sympy_args(mathics_args: BaseElement, evaluation) -> list: """ Convert Mathics arguments, such as the arguments in an evaluation method a Python list that is sutiable for feeding as arguments @@ -268,6 +280,7 @@ def to_numeric_sympy_args(mathics_args: Type[BaseElement], evaluation) -> list: from mathics.eval.numerify import numerify if mathics_args.is_literal: + assert hasattr(mathics_args, "value") sympy_args = [mathics_args.value] else: args = numerify(mathics_args, evaluation).get_sequence() @@ -352,7 +365,7 @@ def old_from_sympy(expr) -> BaseElement: if expr.is_Symbol: name = str(expr) if isinstance(expr, sympy.Dummy): - name = name + (f"__Dummy_{expr.dummy_index}") + name = name + (f"__Dummy_{expr.dummy_index}") # type: ignore[attr-defined] # Probably, this should be the value attribute return Symbol(name, sympy_dummy=expr) if is_Cn_expr(name): @@ -360,8 +373,8 @@ def old_from_sympy(expr) -> BaseElement: if name.startswith(sympy_symbol_prefix): name = name[len(sympy_symbol_prefix) :] if name.startswith(sympy_slot_prefix): - index = name[len(sympy_slot_prefix) :] - return Expression(SymbolSlot, Integer(int(index))) + index = int(name[len(sympy_slot_prefix) :]) + return Expression(SymbolSlot, Integer(index)) elif expr.is_NumberSymbol: name = str(expr) if name is not None: @@ -427,13 +440,14 @@ def old_from_sympy(expr) -> BaseElement: return expr.expr if isinstance(expr, sympy.Piecewise): - args = expr.args return Expression( SymbolPiecewise, ListExpression( *[ to_mathics_list(from_sympy(case), from_sympy(cond)) - for case, cond in args + for case, cond in cast( + Sequence[Tuple[sympy.Basic, sympy.Basic]], expr.args + ) ] ), ) @@ -441,11 +455,11 @@ def old_from_sympy(expr) -> BaseElement: if isinstance(expr, SympyPrime): return Expression(SymbolPrime, from_sympy(expr.args[0])) if isinstance(expr, sympy.RootSum): - return Expression(SymbolRootSum, from_sympy(expr.poly), from_sympy(expr.fun)) + return Expression(SymbolRootSum, from_sympy(expr.poly), from_sympy(expr.fun)) # type: ignore[attr-defined] if isinstance(expr, sympy.PurePoly): coeffs = expr.coeffs() monoms = expr.monoms() - result = [] + result: List[BaseElement] = [] for coeff, monom in zip(coeffs, monoms): factors = [] if coeff != 1: @@ -500,12 +514,14 @@ def old_from_sympy(expr) -> BaseElement: else: margs.append(from_sympy(arg)) builtin = sympy_to_mathics.get(name) - return builtin.from_sympy(margs) + assert builtin is not None + return builtin.from_sympy(tuple(margs)) elif isinstance(expr, sympy.sign): name = "Sign" else: name = expr.func.__name__ + assert name is not None if is_Cn_expr(name): return Expression( Expression(Symbol("C"), Integer(int(name[1:]))), @@ -516,7 +532,7 @@ def old_from_sympy(expr) -> BaseElement: args = [from_sympy(arg) for arg in expr.args] builtin = sympy_to_mathics.get(name) if builtin is not None: - return builtin.from_sympy(args) + return builtin.from_sympy(tuple(args)) return Expression(Symbol(name), *args) if isinstance(expr, sympy.Tuple): diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 55d5fefbc..bb6c7109b 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -7,7 +7,7 @@ import re from collections import defaultdict from os.path import join as osp_join -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Sequence, Set, Tuple from mathics_scanner.tokeniser import full_names_pattern @@ -26,10 +26,10 @@ type_compiled_pattern = type(re.compile("a.a")) # The contents of $OutputForms. FormMeta in mathics.base.forms adds to this. -OutputForms = set() +OutputForms: Set[Symbol] = set() # The contents of $PrintForms. FormMeta in mathics.base.forms adds to this. -PrintForms = set() +PrintForms: Set[Symbol] = set() def get_file_time(file) -> float: @@ -114,13 +114,13 @@ def __init__( self.builtin: Dict[str, Definition] = {} self.user: Dict[str, Definition] = {} self.pymathics: Dict[str, Definition] = {} - self.definitions_cache = {} - self.lookup_cache = {} - self.proxy = defaultdict(set) + self.definitions_cache: Dict[str, Definition] = {} + self.lookup_cache: Dict[str, str] = {} + self.proxy: Dict[str, Set[str]] = defaultdict(set) self.now = 0 # increments whenever something is updated - self._packages = [] + self._packages: List[str] = [] self.current_context = "Global`" - self.context_path = ( + self.context_path: Tuple[str, ...] = ( "System`", "Global`", ) @@ -151,8 +151,8 @@ def __init__( ] builtin_time = max(builtin_dates) if get_file_time(builtin_filename) > builtin_time: - builtin_file = open(builtin_filename, "rb") - self.builtin = pickle.load(builtin_file) + with open(builtin_filename, "rb") as builtin_file: + self.builtin = pickle.load(builtin_file) loaded = True if not loaded: definition_contribute(self) @@ -165,8 +165,8 @@ def __init__( raise if builtin_filename is not None: - builtin_file = open(builtin_filename, "wb") - pickle.dump(self.builtin, builtin_file, -1) + with open(builtin_filename, "wb") as builtin_file: + pickle.dump(self.builtin, builtin_file, -1) autoload_files(self, ROOT_DIR, "autoload") @@ -231,12 +231,7 @@ def is_uncertain_final_value(self, last_evaluated_time: int, symbols: set) -> bo pass else: # Get timestamp for the most-recently changed part of the given expression. - symbol_change_time = getattr(symbol, "changed", None) - if symbol_change_time is None: - # Must be a system symbol that never changes. - # FIXME: couldn't this initially start out 0 so no test is needed? - symbol.change_timestamp = 0 - elif symbol_change_time > last_evaluated_time: + if symbol.changed > last_evaluated_time: return True return False @@ -256,14 +251,13 @@ def set_current_context(self, context) -> None: self.current_context = context self.clear_cache() - def set_context_path(self, context_path) -> None: - assert isinstance(context_path, list) + def set_context_path(self, context_path: Sequence[str]) -> None: assert all([isinstance(c, str) for c in context_path]) self.set_ownvalue( "System`$ContextPath", to_mathics_list(*context_path, elements_conversion_fn=String), ) - self.context_path = context_path + self.context_path = tuple(context_path) self.clear_cache() def set_inputfile(self, dir: str) -> None: @@ -413,7 +407,7 @@ def in_ctx(name, ctx): def have_definition(self, name) -> bool: return self.get_definition(name, only_if_exists=True) is not None - def get_definition(self, name: str, only_if_exists=False) -> "Definition": + def get_definition(self, name: str, only_if_exists=False) -> Optional["Definition"]: definition = self.definitions_cache.get(name, None) if definition is not None: return definition @@ -451,7 +445,7 @@ def get_definition(self, name: str, only_if_exists=False) -> "Definition": attributes = A_NO_ATTRIBUTES options = {} - formatvalues = { + formatvalues: Dict[str, list] = { "": [], } # Merge definitions @@ -557,7 +551,7 @@ def get_user_definition(self, name, create=True) -> Optional["Definition"]: self.clear_cache(name) return self.user[name] - def mark_changed(self, definition) -> None: + def mark_changed(self, definition: "Definition") -> None: self.now += 1 definition.changed = self.now @@ -577,21 +571,23 @@ def add_user_definition(self, name, definition) -> None: def set_attribute(self, name, attribute) -> None: definition = self.get_user_definition(self.lookup_name(name)) - definition.attributes |= attribute - - self.mark_changed(definition) + if definition is not None: + definition.attributes |= attribute + self.mark_changed(definition) self.clear_definitions_cache(name) def set_attributes(self, name, attributes) -> None: definition = self.get_user_definition(self.lookup_name(name)) - definition.attributes = attributes - self.mark_changed(definition) + if definition is not None: + definition.attributes = attributes + self.mark_changed(definition) self.clear_definitions_cache(name) def clear_attribute(self, name, attribute) -> None: definition = self.get_user_definition(self.lookup_name(name)) - definition.attributes &= ~attribute - self.mark_changed(definition) + if definition is not None: + definition.attributes &= ~attribute + self.mark_changed(definition) self.clear_definitions_cache(name) def add_rule(self, name, rule, position=None): @@ -610,36 +606,41 @@ def add_format(self, name, rule, form="") -> None: forms = form else: forms = [form] - for form in forms: - if form not in definition.formatvalues: - definition.formatvalues[form] = [] - insert_rule(definition.formatvalues[form], rule) - self.mark_changed(definition) + if definition is not None: + for form in forms: + if form not in definition.formatvalues: + definition.formatvalues[form] = [] + insert_rule(definition.formatvalues[form], rule) + self.mark_changed(definition) self.clear_definitions_cache(name) def add_nvalue(self, name, rule) -> None: definition = self.get_user_definition(self.lookup_name(name)) - definition.add_rule_at(rule, "n") - self.mark_changed(definition) + if definition is not None: + definition.add_rule_at(rule, "n") + self.mark_changed(definition) self.clear_definitions_cache(name) def add_default(self, name, rule) -> None: definition = self.get_user_definition(self.lookup_name(name)) - definition.add_rule_at(rule, "default") - self.mark_changed(definition) + if definition is not None: + definition.add_rule_at(rule, "default") + self.mark_changed(definition) self.clear_definitions_cache(name) def add_message(self, name, rule) -> None: definition = self.get_user_definition(self.lookup_name(name)) - definition.add_rule_at(rule, "messages") - self.mark_changed(definition) + if definition is not None: + definition.add_rule_at(rule, "messages") + self.mark_changed(definition) self.clear_definitions_cache(name) def set_values(self, name, values, rules) -> None: pos = valuesname(values) definition = self.get_user_definition(self.lookup_name(name)) - definition.set_values_list(pos, rules) - self.mark_changed(definition) + if definition is not None: + definition.set_values_list(pos, rules) + self.mark_changed(definition) self.clear_definitions_cache(name) def get_options(self, name): @@ -676,8 +677,9 @@ def set_ownvalue(self, name, value) -> None: def set_options(self, name, options) -> None: definition = self.get_user_definition(self.lookup_name(name)) - definition.options = options - self.mark_changed(definition) + if definition is not None: + definition.options = options + self.mark_changed(definition) self.clear_definitions_cache(name) def unset(self, name, expr): @@ -736,7 +738,7 @@ def get_tag_position(pattern, name) -> Optional[str]: "System`BlankNullSequence", ) - def strip_pattern_name_and_condition(pat: BasePattern) -> ExpressionPattern: + def strip_pattern_name_and_condition(pat: BasePattern) -> BasePattern: """ In ``Pattern[name_, pattern_]`` and ``Condition[pattern_, cond_]`` @@ -752,17 +754,19 @@ def strip_pattern_name_and_condition(pat: BasePattern) -> ExpressionPattern: if not hasattr(pat, "head"): return pat - # We have to use get_head_name() below because - # pat can either SymbolCondition or . - # In the latter case, comparing to SymbolCondition is not sufficient. - if pat.get_head_name() == "System`Condition": - if len(pat.elements) > 1: - return strip_pattern_name_and_condition(pat.elements[0]) - # The same kind of get_head_name() check is needed here as well and - # is not the same as testing against SymbolPattern. - if pat.get_head_name() == "System`Pattern": - if len(pat.elements) == 2: - return strip_pattern_name_and_condition(pat.elements[1]) + if hasattr(pat, "elements"): + # We have to use get_head_name() below because + # pat can either SymbolCondition or . + # In the latter case, comparing to SymbolCondition is not sufficient. + if pat.get_head_name() == "System`Condition": + if len(pat.elements) > 1: + return strip_pattern_name_and_condition(pat.elements[0]) + # The same kind of get_head_name() check is needed here as well and + # is not the same as testing against SymbolPattern. + if pat.get_head_name() == "System`Pattern": + if len(pat.elements) == 2: + return strip_pattern_name_and_condition(pat.elements[1]) + return pat def is_pattern_a_kind_of(pattern: ExpressionPattern, pattern_name: str) -> bool: @@ -784,6 +788,7 @@ def is_pattern_a_kind_of(pattern: ExpressionPattern, pattern_name: str) -> bool: if head_name in blanks: if isinstance(head, Symbol): return False + assert hasattr(head, "elements") sub_elements = head.elements if len(sub_elements) == 1: head_name = head.elements[0].get_name() @@ -930,6 +935,7 @@ def __init__( self.nvalues = nvalues self.defaultvalues = defaultvalues self.builtin = builtin + self.changed = 0 for rule in rules: if not self.add_rule(rule): print(f"{rule.pattern.expr} could not be associated to {self.name}") diff --git a/mathics/core/element.py b/mathics/core/element.py index 6b7818ba9..080875dc4 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -6,10 +6,13 @@ """ from abc import ABC -from typing import Any, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Tuple, Union from mathics.core.attributes import A_NO_ATTRIBUTES +if TYPE_CHECKING: + from mathics.core.evaluation import Evaluation + def ensure_context(name: str, context="System`") -> str: assert isinstance(name, str) @@ -38,7 +41,7 @@ def fully_qualified_symbol_name(name) -> bool: try: - from recordclass import RecordClass + from recordclass import RecordClass # type: ignore[import-not-found] # Note: Something in cythonization barfs if we put this in # Expression and you try to call this like @@ -82,7 +85,7 @@ class ElementsProperties(RecordClass): from dataclasses import dataclass @dataclass - class ElementsProperties: + class ElementsProperties: # type: ignore[no-redef] """Properties of Expression elements that are useful in evaluation. In general, if you have some set of properties that you know should @@ -205,6 +208,7 @@ class BaseElement(KeyComparable, ABC): Some important subclasses: Atom and Expression. """ + options: dict last_evaluated: Any # this variable holds a function defined in mathics.core.expression that creates an expression create_expression: Any @@ -247,7 +251,7 @@ def equal2(self, rhs: Any) -> Optional[bool]: return self == rhs return None - def format(self, evaluation, form, **kwargs) -> "BoxElementMixin": + def format(self, evaluation, form, **kwargs) -> Optional["BaseElement"]: from mathics.core.symbols import Symbol from mathics.eval.makeboxes import format_element @@ -331,7 +335,7 @@ def get_precision(self) -> Optional[int]: """ return None - def get_sequence(self) -> tuple: + def get_sequence(self) -> Sequence["BaseElement"]: """ If ``self`` is a Mathics3 Sequence, return its elements. Otherwise, just return self wrapped in a tuple @@ -350,7 +354,7 @@ def get_sequence(self) -> tuple: # for the expression "F[{a,b}]" this function is expected to return: # ListExpression[Symbol(a), Symbol(b)]. if self.get_head() is SymbolSequence: - return self.elements + return self.get_elements() else: return tuple([self]) @@ -474,7 +478,9 @@ def is_literal(self) -> bool: """ return False - def rewrite_apply_eval_step(self, evaluation) -> Tuple["BaseElement", bool]: + def rewrite_apply_eval_step( + self, evaluation + ) -> Tuple[Optional["BaseElement"], bool]: """ Performs a since rewrite/apply/eval step used in evaluation. diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 6a010a54f..9ff5d4af0 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -6,7 +6,7 @@ from abc import ABC from queue import Queue from threading import Thread, stack_size as set_thread_stack_size -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union, overload from mathics_scanner import TranslateError @@ -378,9 +378,17 @@ def get_stored_result(self, eval_result, output_forms): def stop(self) -> None: self.stopped = True + @overload + def format_output(self, expr: BaseElement, format: Optional[dict] = None) -> dict: + ... + + @overload def format_output( self, expr: BaseElement, format: Optional[str] = None ) -> Union[BaseElement, str, None]: + ... + + def format_output(self, expr, format=None): """ This function takes an expression `expr` and a format `format`. If `format` is None, then returns `expr`. Otherwise, diff --git a/mathics/core/formatter.py b/mathics/core/formatter.py index 19176fbb8..0563ab1c9 100644 --- a/mathics/core/formatter.py +++ b/mathics/core/formatter.py @@ -124,7 +124,10 @@ def add_conversion_fn(cls, module_fn_name=None) -> None: We use frame introspection to get all of this done. """ - fr = inspect.currentframe().f_back + fr = inspect.currentframe() + assert fr is not None + fr = fr.f_back + assert fr is not None module_dict = fr.f_globals # The last part of the module name is expected to be the conversion routine. diff --git a/mathics/core/list.py b/mathics/core/list.py index f32c3dc80..496779189 100644 --- a/mathics/core/list.py +++ b/mathics/core/list.py @@ -146,6 +146,7 @@ def rewrite_apply_eval_step(self, evaluation) -> Tuple[Expression, bool]: if self.elements_properties is None: self._build_elements_properties() + assert self.elements_properties is not None if not self.elements_properties.elements_fully_evaluated: new = self.shallow_copy().evaluate_elements(evaluation) return new, False diff --git a/mathics/core/load_builtin.py b/mathics/core/load_builtin.py index 34f19c9ea..5e255e2ab 100644 --- a/mathics/core/load_builtin.py +++ b/mathics/core/load_builtin.py @@ -14,7 +14,7 @@ import pkgutil from glob import glob from types import ModuleType -from typing import Dict, List, Optional, Set +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from mathics.core.convert.sympy import mathics_to_sympy, sympy_to_mathics from mathics.core.pattern import pattern_objects @@ -22,6 +22,9 @@ from mathics.eval.makeboxes import builtins_precedence from mathics.settings import ENABLE_FILES_MODULE +if TYPE_CHECKING: + from mathics.core.builtin import Builtin + # List of Python modules contain Mathics3 Builtins. # This list used outside to gather documentation, # and test module consistency. It is @@ -43,7 +46,9 @@ display_operators_set: Set[str] = set() -def add_builtins_from_builtin_module(module: ModuleType, builtins_list: list): +def add_builtins_from_builtin_module( + module: ModuleType, builtins_list: List[Tuple[str, "Builtin"]] +): """ Process a modules which contains Builtin classes so that the class is imported in the Python sense but also that we @@ -70,7 +75,7 @@ class is imported in the Python sense but also that we def add_builtins_from_builtin_modules(modules: List[ModuleType]): - builtins_list = [] + builtins_list: List[Tuple[str, "Builtin"]] = [] for module in modules: add_builtins_from_builtin_module(module, builtins_list) add_builtins(builtins_list) @@ -79,7 +84,7 @@ def add_builtins_from_builtin_modules(modules: List[ModuleType]): # The fact that we are importing inside here, suggests add_builtins # should get moved elsewhere. -def add_builtins(new_builtins): +def add_builtins(new_builtins: List[Tuple[str, "Builtin"]]): from mathics.core.builtin import ( Operator, PatternObject, diff --git a/mathics/core/number.py b/mathics/core/number.py index 0a075b2a6..3f598f9c4 100644 --- a/mathics/core/number.py +++ b/mathics/core/number.py @@ -6,7 +6,7 @@ from sys import float_info from typing import List, Optional, Union -import mpmath +import mpmath # type: ignore[import-untyped] import sympy from mathics.core.element import BaseElement @@ -83,7 +83,7 @@ def get_precision( """ if value is SymbolMachinePrecision: return None - else: + elif hasattr(value, "round_to_float"): from mathics.core.atoms import MachineReal dmin = _get_float_inf(SymbolMinPrecision, evaluation) @@ -106,6 +106,7 @@ def get_precision( else: return d raise PrecisionValueError() + return None def get_type(value) -> Optional[str]: diff --git a/mathics/core/parser/convert.py b/mathics/core/parser/convert.py index b53caa512..4e9a40d9c 100644 --- a/mathics/core/parser/convert.py +++ b/mathics/core/parser/convert.py @@ -4,7 +4,7 @@ """ from math import log10 -from typing import Tuple +from typing import Optional, Tuple import sympy @@ -111,14 +111,10 @@ def convert_Number(self, node: AST_Number) -> tuple: # For 0, a finite absolute precision even if # the number is an integer, it is stored as a # PrecisionReal number. - if x == 0: - prec10 = acc - else: - prec10 = acc + log10(abs(x)) return ( "PrecisionReal", ("DecimalString", str("-" + s if sign == -1 else s)), - prec10, + acc + log10(abs(x)) if x != 0 else acc, ) else: # A single Reversed Prime ("`") represents a fixed precision @@ -156,6 +152,7 @@ def convert_Number(self, node: AST_Number) -> tuple: x = float(sympy.Rational(p, q)) # determine `prec10` the digits of precision in base 10 + prec10: Optional[float] if suffix is None: acc = len(s[1]) acc10 = acc * log10(base) diff --git a/mathics/core/parser/parser.py b/mathics/core/parser/parser.py index 84aada633..6dce91f88 100644 --- a/mathics/core/parser/parser.py +++ b/mathics/core/parser/parser.py @@ -325,7 +325,7 @@ def p_RawLeftAssociation(self, token) -> Node: self.bracket_depth -= 1 return Node("Association", *seq) - def p_LeftRowBox(self, token) -> Node: + def p_LeftRowBox(self, token) -> Union[Node, String]: self.consume() children = [] self.box_depth += 1 @@ -335,6 +335,7 @@ def p_LeftRowBox(self, token) -> Node: newnode = self.parse_box(0) children.append(newnode) token = self.next() + result: Union[Node, String] if len(children) == 0: result = NullString elif len(children) == 1: @@ -489,15 +490,15 @@ def p_Out(self, token) -> Node: def p_Slot(self, token) -> Node: self.consume() - text = token.text - if len(text) == 1: + text = token.text[1:] + n: Union[Number, String] + if text == "": n = Number1 else: - n = text[1:] - if n.isdigit(): - n = Number(n) + if text.isdigit(): + n = Number(text) else: - n = String(n) + n = String(text) return Node("Slot", n) def p_SlotSequence(self, token) -> Node: diff --git a/mathics/core/pattern.py b/mathics/core/pattern.py index dc5d247bb..d16847440 100644 --- a/mathics/core/pattern.py +++ b/mathics/core/pattern.py @@ -16,7 +16,7 @@ from abc import ABC from itertools import chain -from typing import Callable, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Callable, Dict, Optional, Sequence, Tuple, Type, Union from mathics.core.atoms import Integer from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_ORDERLESS @@ -40,6 +40,9 @@ ) from mathics.core.util import permutations, subranges, subsets +if TYPE_CHECKING: + from mathics.core.builtin import PatternObject + SYSTEM_SYMBOLS_PATTERNS = symbol_set( SymbolAlternatives, SymbolBlank, @@ -54,7 +57,7 @@ SymbolRepeatedNull, ) -pattern_objects = {} +pattern_objects: Dict[str, Type["PatternObject"]] = {} class StopGenerator(Exception): @@ -189,7 +192,9 @@ def create( return pattern_object(expr, evaluation=evaluation) if isinstance(expr, Atom): return AtomPattern(expr, evaluation) - return ExpressionPattern(expr, attributes, evaluation) + if isinstance(expr, Expression): + return ExpressionPattern(expr, attributes, evaluation) + raise TypeError(f"Cannot create Pattern for {expr}") def get_attributes(self, definitions): """The attributes of the expression""" @@ -300,6 +305,9 @@ def get_match_candidates( """ return tuple() + def get_match_count(self, vars_dict: Optional[dict] = None) -> Tuple[int, int]: + raise NotImplementedError + def get_match_candidates_count( self, elements: Tuple[BaseElement], pattern_context: dict ) -> Union[int, tuple]: @@ -308,6 +316,8 @@ def get_match_candidates_count( def sameQ(self, other: BaseElement) -> bool: """Mathics SameQ""" + if not isinstance(other, BasePattern): + return False return self.expr.sameQ(other.expr) @@ -323,8 +333,8 @@ def __init__(self, expr: Atom, evaluation: Optional[Evaluation] = None) -> None: self.expr = expr self.atom = expr if isinstance(expr, Symbol): - self.match = self.match_symbol - self.get_match_candidates = self.get_match_symbol_candidates + self.match = self.match_symbol # type: ignore[method-assign] + self.get_match_candidates = self.get_match_symbol_candidates # type: ignore[method-assign] def __repr__(self): return f"" @@ -436,6 +446,7 @@ def match(self, expression: BaseElement, pattern_context: dict): self.__set_pattern_attributes__( self.head.get_attributes(evaluation.definitions) ) + assert self.attributes is not None attributes = self.attributes if not A_FLAT & attributes: @@ -448,21 +459,17 @@ def match(self, expression: BaseElement, pattern_context: dict): parms.setdefault("element_index", None) parms.setdefault("element_count", None) - if not isinstance(expression, Atom): + if isinstance(expression, Expression): try: - basic_match_expression( - self, - expression, - parms, - ) + basic_match_expression(self, expression, parms) except StopGenerator_ExpressionPattern_match: return if A_ONE_IDENTITY & attributes: - match_expression_with_one_identity(self, expression=expression, parms=parms) + match_expression_with_one_identity(self, expression, parms) def _get_pre_choices( - self, expression: BaseElement, yield_choice: Callable, pattern_context: dict + self, expression: Expression, yield_choice: Callable, pattern_context: dict ): """ If not Orderless, call yield_choice with vars as the parameter. @@ -523,7 +530,7 @@ def get_wrappings(self, yield_func: Callable, items: Tuple, pattern_context: dic def match_element( self, - element: BaseElement, + element: BasePattern, pattern_context, ): """Try to match an element.""" @@ -557,6 +564,7 @@ def match_element( element.get_head() in SYSTEM_SYMBOLS_PATTERNS ) + set_lengths: Tuple[int, Optional[int]] if try_flattened: set_lengths = (match_count[0], None) else: @@ -566,21 +574,19 @@ def match_element( # into one operand may occur. # This can of course also be when flat and same head. try_flattened = try_flattened or ( - A_FLAT & attributes and element.get_head() == expression.head + A_FLAT & attributes and element.get_head() == expression.get_head() ) less_first = len(rest_elements) > 0 if A_ORDERLESS & attributes: - parms = { - "expression": expression, - "element": element, - "vars_dict": vars_dict, - "attributes": attributes, - } - sets = expression_pattern_match_element_orderless( - parms, + { + "expression": expression, + "element": element, + "vars_dict": vars_dict, + "attributes": attributes, + }, candidates, element_candidates, less_first, @@ -660,8 +666,8 @@ def sort(self): def match_expression_with_one_identity( - self: BasePattern, - expression: Expression, + self: ExpressionPattern, + expression: BaseElement, parms: dict, ): """ @@ -674,14 +680,15 @@ def match_expression_with_one_identity( # This tries to reduce the pattern to a non empty # set of default values, and a single pattern. from mathics.builtin.patterns.composite import Pattern + from mathics.core.builtin import PatternObject vars_dict: dict = parms["vars_dict"] evaluation: Evaluation = parms["evaluation"] default_indx: int = 0 optionals: dict = {} - new_pattern: Optional[Pattern] = None - pattern_head: Expression = self.head.expr + new_pattern: Optional[BasePattern] = None + pattern_head: BaseElement = self.head.expr for pat_elem in self.elements: default_indx += 1 if isinstance(pat_elem, AtomPattern): @@ -690,11 +697,14 @@ def match_expression_with_one_identity( new_pattern = pat_elem # TODO: check into account the second argument, # and if there is a default value... - elif pat_elem.get_head_name() == "System`Optional": + elif ( + isinstance(pat_elem, PatternObject) + and pat_elem.get_head() == SymbolOptional + ): if len(pat_elem.elements) == 2: pat, value = pat_elem.elements if isinstance(pat, Pattern): - key = pat.elements[0].atom.name + key = pat.elements[0].atom.name # type: ignore[attr-defined] else: # if the first element of the Optional # is not a `Pattern`, then we need to @@ -704,22 +714,23 @@ def match_expression_with_one_identity( elif len(pat_elem.elements) == 1: pat = pat_elem.elements[0] if isinstance(pat, Pattern): - key = pat.elements[0].atom.name + key = pat.elements[0].atom.name # type: ignore[attr-defined] else: key = "" # Now, determine the default value defaultvalue_expr = Expression( SymbolDefault, pattern_head, Integer(default_indx) ) - value = defaultvalue_expr.evaluate(evaluation) - if value.sameQ(defaultvalue_expr): + result = defaultvalue_expr.evaluate(evaluation) + assert result is not None + if result.sameQ(defaultvalue_expr): return - optionals[key] = value + optionals[key] = result else: return + elif new_pattern is not None: + return else: - if new_pattern is not None: - return new_pattern = pat_elem # If there is not optional values in the pattern, then @@ -744,6 +755,7 @@ def match_expression_with_one_identity( # TODO: remove me eventually del parms["attributes"] + assert new_pattern is not None new_pattern.match(expression=expression, pattern_context=parms) @@ -878,7 +890,7 @@ def expression_pattern_match_element_orderless( candidates: tuple, element_candidates: Union[tuple, set], less_first: bool, - set_lengths: tuple, + set_lengths: Tuple[int, Optional[int]], ): """ match element for orderless expressions @@ -1072,7 +1084,7 @@ def get_pre_choices_orderless( # prev_element = None # count duplicate elements - expr_groups = {} + expr_groups: Dict[BaseElement, int] = {} for element in expression.elements: expr_groups[element] = expr_groups.get(element, 0) + 1 diff --git a/mathics/core/rules.py b/mathics/core/rules.py index 52c3013f0..2ed95d0c8 100644 --- a/mathics/core/rules.py +++ b/mathics/core/rules.py @@ -94,7 +94,7 @@ class BaseRule(KeyComparable, ABC): def __init__( self, - pattern: Expression, + pattern: BaseElement, system: bool = False, evaluation: Optional[Evaluation] = None, attributes: Optional[int] = None, @@ -229,7 +229,7 @@ class Rule(BaseRule): def __init__( self, - pattern: Expression, + pattern: BaseElement, replace: BaseElement, system=False, evaluation: Optional[Evaluation] = None, diff --git a/mathics/core/streams.py b/mathics/core/streams.py index 8f9b5a81e..1fe46a7aa 100644 --- a/mathics/core/streams.py +++ b/mathics/core/streams.py @@ -8,7 +8,7 @@ import sys import tempfile from io import open as io_open -from typing import List, Optional, Tuple +from typing import Dict, List, Optional, Tuple import requests @@ -54,7 +54,7 @@ def urlsave_tmp(url, location=None, **kwargs): return None -def path_search(filename: str) -> Tuple[str, bool]: +def path_search(filename: str) -> Tuple[Optional[str], bool]: """ Search for a Mathics `filename` possibly adding extensions ".mx", or ".m" or as a file under directory PATH_VAR or as an Internet address. @@ -70,8 +70,7 @@ def path_search(filename: str) -> Tuple[str, bool]: for ext in [".mx", ".m"]: result, is_temporary_file = path_search(filename + ext) if result is not None: - filename = None - break + return result, is_temporary_file if filename is not None: result = None # If filename is an Internet address, download the file @@ -169,7 +168,7 @@ def __exit__(self, type, value, traceback): class StreamsManager: __instance = None - STREAMS = {} + STREAMS: Dict[int, Stream] = {} @staticmethod def get_instance(): diff --git a/mathics/core/subexpression.py b/mathics/core/subexpression.py index 4fb1846ef..48b351586 100644 --- a/mathics/core/subexpression.py +++ b/mathics/core/subexpression.py @@ -278,14 +278,6 @@ def elements(self): def elements(self, value): raise ValueError("SubExpression.elements is write protected.") - @property - def elements(self): - return self._elementsp - - @elements.setter - def elements(self, value): - raise ValueError("SubExpression.elements is write protected.") - def to_expression(self): return Expression( self._headp.to_expression(), diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index 3f4c62222..536b759f6 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import time -from typing import Any, Dict, FrozenSet, List, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Optional, Sequence, Union from mathics.core.element import ( BaseElement, @@ -11,6 +11,9 @@ fully_qualified_symbol_name, ) +if TYPE_CHECKING: + from mathics.core.atoms import String + # I put this constants here instead of inside `mathics.core.convert.sympy` # to avoid a circular reference. Maybe they should be in its own module. diff --git a/mathics/core/util.py b/mathics/core/util.py index 4a1be908f..82c209364 100644 --- a/mathics/core/util.py +++ b/mathics/core/util.py @@ -7,6 +7,7 @@ from itertools import chain from pathlib import PureWindowsPath from platform import python_implementation +from typing import Optional IS_PYPY = python_implementation() == "PyPy" @@ -44,13 +45,12 @@ def permutations(items): # already_taken.add(item) -def subsets(items, min, max, included=None, less_first=False): +def subsets(items, min: int, max: Optional[int], included=None, less_first=False): if max is None: max = len(items) lengths = list(range(min, max + 1)) if not less_first: - lengths = reversed(lengths) - lengths = list(lengths) + lengths = list(reversed(lengths)) if lengths and lengths[0] == 0: lengths = lengths[1:] + [0]