1# -*- coding: utf-8 -*- 2# cython: language_level=3 3 4import re 5import sympy 6from functools import total_ordering 7import importlib 8from itertools import chain 9import typing 10from typing import Any, cast 11 12from mathics.version import __version__ # noqa used in loading to check consistency. 13 14from mathics.core.convert import from_sympy 15from mathics.core.definitions import Definition 16from mathics.core.parser.util import SystemDefinitions, PyMathicsDefinitions 17from mathics.core.rules import Rule, BuiltinRule, Pattern 18from mathics.core.expression import ( 19 BaseExpression, 20 Expression, 21 Integer, 22 MachineReal, 23 PrecisionReal, 24 String, 25 Symbol, 26 SymbolTrue, 27 SymbolFalse, 28 ensure_context, 29 strip_context, 30) 31from mathics.core.numbers import get_precision, PrecisionValueError 32 33 34def get_option(options, name, evaluation, pop=False, evaluate=True): 35 # we do not care whether an option X is given as System`X, 36 # Global`X, or with any prefix from $ContextPath for that 37 # matter. Also, the quoted string form "X" is ok. all these 38 # variants name the same option. this matches Wolfram Language 39 # behaviour. 40 name = strip_context(name) 41 contexts = (s + "%s" for s in evaluation.definitions.get_context_path()) 42 43 for variant in chain(contexts, ('"%s"',)): 44 resolved_name = variant % name 45 if pop: 46 value = options.pop(resolved_name, None) 47 else: 48 value = options.get(resolved_name) 49 if value is not None: 50 return value.evaluate(evaluation) if evaluate else value 51 return None 52 53 54def has_option(options, name, evaluation): 55 return get_option(options, name, evaluation, evaluate=False) is not None 56 57 58mathics_to_python = {} 59 60 61class Builtin(object): 62 name: typing.Optional[str] = None 63 context = "" 64 abstract = False 65 attributes: typing.Tuple[Any, ...] = () 66 rules: typing.Dict[str, Any] = {} 67 formats: typing.Dict[str, Any] = {} 68 messages: typing.Dict[str, Any] = {} 69 options: typing.Dict[str, Any] = {} 70 defaults = {} 71 72 def __new__(cls, *args, **kwargs): 73 if kwargs.get("expression", None) is not False: 74 return Expression(cls.get_name(), *args) 75 else: 76 instance = super().__new__(cls) 77 if not instance.formats: 78 # Reset formats so that not every instance shares the same 79 # empty dict {} 80 instance.formats = {} 81 return instance 82 83 def __init__(self, *args, **kwargs): 84 super().__init__() 85 if hasattr(self, "python_equivalent"): 86 mathics_to_python[self.get_name()] = self.python_equivalent 87 88 def contribute(self, definitions, is_pymodule=False): 89 from mathics.core.parser import parse_builtin_rule 90 91 # Set the default context 92 if not self.context: 93 self.context = "Pymathics`" if is_pymodule else "System`" 94 95 name = self.get_name() 96 options = {} 97 option_syntax = "Warn" 98 99 for option, value in self.options.items(): 100 if option == "$OptionSyntax": 101 option_syntax = value 102 continue 103 option = ensure_context(option) 104 options[option] = parse_builtin_rule(value) 105 if option.startswith("System`"): 106 # Create a definition for the option's symbol. 107 # Otherwise it'll be created in Global` when it's 108 # used, so it won't work. 109 if option not in definitions.builtin: 110 definitions.builtin[option] = Definition( 111 name=name, attributes=set() 112 ) 113 114 # Check if the given options are actually supported by the Builtin. 115 # If not, we might issue an optx error and abort. Using '$OptionSyntax' 116 # in your Builtin's 'options', you can specify the exact behaviour 117 # using one of the following values: 118 119 # - 'Strict': warn and fail with unsupported options 120 # - 'Warn': warn about unsupported options, but continue 121 # - 'Ignore': allow unsupported options, do not warn 122 123 if option_syntax in ("Strict", "Warn", "System`Strict", "System`Warn"): 124 125 def check_options(options_to_check, evaluation): 126 name = self.get_name() 127 for key, value in options_to_check.items(): 128 short_key = strip_context(key) 129 if not has_option(options, short_key, evaluation): 130 evaluation.message( 131 name, 132 "optx", 133 Expression("Rule", short_key, value), 134 strip_context(name), 135 ) 136 if option_syntax in ("Strict", "System`Strict"): 137 return False 138 return True 139 140 elif option_syntax in ("Ignore", "System`Ignore"): 141 check_options = None 142 else: 143 raise ValueError( 144 "illegal option mode %s; check $OptionSyntax." % option_syntax 145 ) 146 147 rules = [] 148 definition_class = ( 149 PyMathicsDefinitions() if is_pymodule else SystemDefinitions() 150 ) 151 152 for pattern, function in self.get_functions(is_pymodule=is_pymodule): 153 rules.append( 154 BuiltinRule(name, pattern, function, check_options, system=True) 155 ) 156 for pattern, replace in self.rules.items(): 157 if not isinstance(pattern, BaseExpression): 158 pattern = pattern % {"name": name} 159 pattern = parse_builtin_rule(pattern, definition_class) 160 replace = replace % {"name": name} 161 # FIXME: Should system=True be system=not is_pymodule ? 162 rules.append(Rule(pattern, parse_builtin_rule(replace), system=True)) 163 164 box_rules = [] 165 if name != "System`MakeBoxes": 166 new_rules = [] 167 for rule in rules: 168 if rule.pattern.get_head_name() == "System`MakeBoxes": 169 box_rules.append(rule) 170 else: 171 new_rules.append(rule) 172 rules = new_rules 173 174 def extract_forms(name, pattern): 175 # Handle a tuple of (forms, pattern) as well as a pattern 176 # on the left-hand side of a format rule. 'forms' can be 177 # an empty string (=> the rule applies to all forms), or a 178 # form name (like 'System`TraditionalForm'), or a sequence 179 # of form names. 180 def contextify_form_name(f): 181 # Handle adding 'System`' to a form name, unless it's 182 # '' (meaning the rule applies to all forms). 183 return "" if f == "" else ensure_context(f) 184 185 if isinstance(pattern, tuple): 186 forms, pattern = pattern 187 if isinstance(forms, str): 188 forms = [contextify_form_name(forms)] 189 else: 190 forms = [contextify_form_name(f) for f in forms] 191 else: 192 forms = [""] 193 return forms, pattern 194 195 formatvalues = {"": []} 196 for pattern, function in self.get_functions("format_"): 197 forms, pattern = extract_forms(name, pattern) 198 for form in forms: 199 if form not in formatvalues: 200 formatvalues[form] = [] 201 formatvalues[form].append( 202 BuiltinRule(name, pattern, function, None, system=True) 203 ) 204 for pattern, replace in self.formats.items(): 205 forms, pattern = extract_forms(name, pattern) 206 for form in forms: 207 if form not in formatvalues: 208 formatvalues[form] = [] 209 if not isinstance(pattern, BaseExpression): 210 pattern = pattern % {"name": name} 211 pattern = parse_builtin_rule(pattern) 212 replace = replace % {"name": name} 213 formatvalues[form].append( 214 Rule(pattern, parse_builtin_rule(replace), system=True) 215 ) 216 for form, formatrules in formatvalues.items(): 217 formatrules.sort() 218 219 messages = [ 220 Rule( 221 Expression("MessageName", Symbol(name), String(msg)), 222 String(value), 223 system=True, 224 ) 225 for msg, value in self.messages.items() 226 ] 227 228 messages.append( 229 Rule( 230 Expression("MessageName", Symbol(name), String("optx")), 231 String("`1` is not a supported option for `2`[]."), 232 system=True, 233 ) 234 ) 235 236 if "Unprotected" in self.attributes: 237 attributes = [] 238 self.attributes = list(self.attributes) 239 self.attributes.remove("Unprotected") 240 else: 241 attributes = ["System`Protected"] 242 243 attributes += list(ensure_context(a) for a in self.attributes) 244 options = {} 245 for option, value in self.options.items(): 246 option = ensure_context(option) 247 options[option] = parse_builtin_rule(value) 248 if option.startswith("System`"): 249 # Create a definition for the option's symbol. 250 # Otherwise it'll be created in Global` when it's 251 # used, so it won't work. 252 if option not in definitions.builtin: 253 definitions.builtin[option] = Definition( 254 name=name, attributes=set() 255 ) 256 defaults = [] 257 for spec, value in self.defaults.items(): 258 value = parse_builtin_rule(value) 259 pattern = None 260 if spec is None: 261 pattern = Expression("Default", Symbol(name)) 262 elif isinstance(spec, int): 263 pattern = Expression("Default", Symbol(name), Integer(spec)) 264 if pattern is not None: 265 defaults.append(Rule(pattern, value, system=True)) 266 267 definition = Definition( 268 name=name, 269 rules=rules, 270 formatvalues=formatvalues, 271 messages=messages, 272 attributes=attributes, 273 options=options, 274 defaultvalues=defaults, 275 builtin=self, 276 ) 277 if is_pymodule: 278 definitions.pymathics[name] = definition 279 else: 280 definitions.builtin[name] = definition 281 282 makeboxes_def = definitions.builtin["System`MakeBoxes"] 283 for rule in box_rules: 284 makeboxes_def.add_rule(rule) 285 286 @classmethod 287 def get_name(cls, short=False) -> str: 288 if cls.name is None: 289 shortname = cls.__name__ 290 else: 291 shortname = cls.name 292 if short: 293 return shortname 294 return cls.context + shortname 295 296 def get_operator(self) -> typing.Optional[str]: 297 return None 298 299 def get_operator_display(self) -> typing.Optional[str]: 300 return None 301 302 def get_functions(self, prefix="apply", is_pymodule=False): 303 from mathics.core.parser import parse_builtin_rule 304 305 unavailable_function = self._get_unavailable_function() 306 for name in dir(self): 307 if name.startswith(prefix): 308 309 function = getattr(self, name) 310 pattern = function.__doc__ 311 if pattern is None: # Fixes PyPy bug 312 continue 313 else: 314 m = re.match(r"([\w,]+)\:\s*(.*)", pattern) 315 if m is not None: 316 attrs = m.group(1).split(",") 317 pattern = m.group(2) 318 else: 319 attrs = [] 320 # if is_pymodule: 321 # name = ensure_context(self.get_name(short=True), "Pymathics") 322 # else: 323 name = self.get_name() 324 pattern = pattern % {"name": name} 325 definition_class = ( 326 PyMathicsDefinitions() if is_pymodule else SystemDefinitions() 327 ) 328 pattern = parse_builtin_rule(pattern, definition_class) 329 if unavailable_function: 330 function = unavailable_function 331 if attrs: 332 yield (attrs, pattern), function 333 else: 334 yield (pattern, function) 335 336 @staticmethod 337 def get_option(options, name, evaluation, pop=False): 338 return get_option(options, name, evaluation, pop) 339 340 def _get_unavailable_function(self): 341 requires = getattr(self, "requires", []) 342 343 for package in requires: 344 try: 345 importlib.import_module(package) 346 except ImportError: 347 348 def apply(**kwargs): # will override apply method 349 kwargs["evaluation"].message( 350 "General", 351 "pyimport", # see inout.py 352 strip_context(self.get_name()), 353 package, 354 ) 355 356 return apply 357 358 return None 359 360 def get_option_string(self, *params): 361 s = self.get_option(*params) 362 if isinstance(s, String): 363 return s.get_string_value(), s 364 elif isinstance(s, Symbol): 365 for prefix in ("Global`", "System`"): 366 if s.get_name().startswith(prefix): 367 return s.get_name()[len(prefix) :], s 368 return None, s 369 370 371class InstanceableBuiltin(Builtin): 372 def __new__(cls, *args, **kwargs): 373 new_kwargs = kwargs.copy() 374 new_kwargs["expression"] = False 375 instance = super().__new__(cls, *args, **new_kwargs) 376 if not instance.formats: 377 # Reset formats so that not every instance shares the same empty 378 # dict {} 379 instance.formats = {} 380 if kwargs.get("expression", None) is not False: 381 try: 382 instance.init(*args, **kwargs) 383 except TypeError: 384 # TypeError occurs when unpickling instance, e.g. PatternObject, 385 # because parameter expr is not given. This should no be a 386 # problem, as pickled objects need their init-method not 387 # being called. 388 pass 389 return instance 390 391 def init(self, *args, **kwargs): 392 pass 393 394 395class AtomBuiltin(Builtin): 396 # allows us to define apply functions, rules, messages, etc. for Atoms 397 # which are by default not in the definitions' contribution pipeline. 398 # see Image[] for an example of this. 399 400 def get_name(self, short=False) -> str: 401 name = super().get_name(short=short) 402 return re.sub(r"Atom$", "", name) 403 404 405class Operator(Builtin): 406 operator: typing.Optional[str] = None 407 precedence: typing.Optional[int] = None 408 precedence_parse = None 409 needs_verbatim = False 410 411 default_formats = True 412 413 def get_operator(self) -> typing.Optional[str]: 414 return self.operator 415 416 def get_operator_display(self) -> typing.Optional[str]: 417 if hasattr(self, "operator_display"): 418 return self.operator_display 419 else: 420 return self.operator 421 422 423class Predefined(Builtin): 424 def get_functions(self, prefix="apply", is_pymodule=False): 425 functions = list(super().get_functions(prefix)) 426 if prefix == "apply" and hasattr(self, "evaluate"): 427 functions.append((Symbol(self.get_name()), self.evaluate)) 428 return functions 429 430 431class SympyObject(Builtin): 432 sympy_name: typing.Optional[str] = None 433 434 mathics_to_sympy = {} 435 436 def __init__(self, *args, **kwargs): 437 super().__init__(*args, **kwargs) 438 if self.sympy_name is None: 439 self.sympy_name = strip_context(self.get_name()).lower() 440 self.mathics_to_sympy[self.__class__.__name__] = self.sympy_name 441 442 def is_constant(self) -> bool: 443 return False 444 445 def get_sympy_names(self) -> typing.List[str]: 446 if self.sympy_name: 447 return [self.sympy_name] 448 return [] 449 450 451class UnaryOperator(Operator): 452 def __init__(self, format_function, *args, **kwargs): 453 super().__init__(*args, **kwargs) 454 name = self.get_name() 455 if self.needs_verbatim: 456 name = "Verbatim[%s]" % name 457 if self.default_formats: 458 op_pattern = "%s[item_]" % name 459 if op_pattern not in self.formats: 460 operator = self.get_operator_display() 461 if operator is not None: 462 form = '%s[{HoldForm[item]},"%s",%d]' % ( 463 format_function, 464 operator, 465 self.precedence, 466 ) 467 self.formats[op_pattern] = form 468 469 470class PrefixOperator(UnaryOperator): 471 def __init__(self, *args, **kwargs): 472 super().__init__("Prefix", *args, **kwargs) 473 474 475class PostfixOperator(UnaryOperator): 476 def __init__(self, *args, **kwargs): 477 super().__init__("Postfix", *args, **kwargs) 478 479 480class BinaryOperator(Operator): 481 grouping = "System`None" # NonAssociative, None, Left, Right 482 483 def __init__(self, *args, **kwargs): 484 super(BinaryOperator, self).__init__(*args, **kwargs) 485 name = self.get_name() 486 # Prevent pattern matching symbols from gaining meaning here using 487 # Verbatim 488 name = "Verbatim[%s]" % name 489 490 # For compatibility, allow grouping symbols in builtins to be 491 # specified without System`. 492 self.grouping = ensure_context(self.grouping) 493 494 if self.grouping in ("System`None", "System`NonAssociative"): 495 op_pattern = "%s[items__]" % name 496 replace_items = "items" 497 else: 498 op_pattern = "%s[x_, y_]" % name 499 replace_items = "x, y" 500 501 if self.default_formats: 502 operator = self.get_operator_display() 503 formatted = 'MakeBoxes[Infix[{%s},"%s",%d,%s], form]' % ( 504 replace_items, 505 operator, 506 self.precedence, 507 self.grouping, 508 ) 509 formatted_output = 'MakeBoxes[Infix[{%s}," %s ",%d,%s], form]' % ( 510 replace_items, 511 operator, 512 self.precedence, 513 self.grouping, 514 ) 515 default_rules = { 516 "MakeBoxes[{0}, form:StandardForm|TraditionalForm]".format( 517 op_pattern 518 ): formatted, 519 "MakeBoxes[{0}, form:InputForm|OutputForm]".format( 520 op_pattern 521 ): formatted_output, 522 } 523 default_rules.update(self.rules) 524 self.rules = default_rules 525 526 527class Test(Builtin): 528 def apply(self, expr, evaluation) -> Symbol: 529 "%(name)s[expr_]" 530 531 if self.test(expr): 532 return SymbolTrue 533 else: 534 return SymbolFalse 535 536 537class SympyFunction(SympyObject): 538 def apply(self, *args): 539 """ 540 Generic apply method that uses the class sympy_name. 541 to call the corresponding sympy function. Arguments are 542 converted to python and the result is converted from sympy 543 """ 544 sympy_args = [a.to_sympy() for a in args] 545 sympy_fn = getattr(sympy, self.sympy_name) 546 return from_sympy(sympy_fn(*sympy_args)) 547 548 def get_constant(self, precision, evaluation, have_mpmath=False): 549 try: 550 d = get_precision(precision, evaluation) 551 except PrecisionValueError: 552 return 553 554 sympy_fn = self.to_sympy() 555 if d is None: 556 result = self.get_mpmath_function() if have_mpmath else sympy_fn() 557 return MachineReal(result) 558 else: 559 return PrecisionReal(sympy_fn.n(d)) 560 561 def get_sympy_function(self, leaves=None): 562 if self.sympy_name: 563 return getattr(sympy, self.sympy_name) 564 return None 565 566 def prepare_sympy(self, leaves): 567 return leaves 568 569 def to_sympy(self, expr, **kwargs): 570 try: 571 if self.sympy_name: 572 leaves = self.prepare_sympy(expr.leaves) 573 sympy_args = [leaf.to_sympy(**kwargs) for leaf in leaves] 574 if None in sympy_args: 575 return None 576 sympy_function = self.get_sympy_function(leaves) 577 return sympy_function(*sympy_args) 578 except TypeError: 579 pass 580 581 def from_sympy(self, sympy_name, leaves): 582 return Expression(self.get_name(), *leaves) 583 584 def prepare_mathics(self, sympy_expr): 585 return sympy_expr 586 587 588class InvalidLevelspecError(Exception): 589 pass 590 591 592class PartError(Exception): 593 pass 594 595 596class PartDepthError(PartError): 597 def __init__(self, index=0): 598 self.index = index 599 600 601class PartRangeError(PartError): 602 pass 603 604 605class BoxConstructError(Exception): 606 pass 607 608 609class BoxConstruct(InstanceableBuiltin): 610 def __new__(cls, *leaves, **kwargs): 611 instance = super().__new__(cls, *leaves, **kwargs) 612 instance._leaves = leaves 613 return instance 614 615 def evaluate(self, evaluation): 616 # THINK about: Should we evaluate the leaves here? 617 return 618 619 def get_head_name(self): 620 return self.get_name() 621 622 def get_lookup_name(self): 623 return self.get_name() 624 625 def get_string_value(self): 626 return "-@" + self.get_head_name() + "@-" 627 628 def sameQ(self, expr) -> bool: 629 """Mathics SameQ""" 630 return expr.sameQ(self) 631 632 def is_atom(self): 633 return False 634 635 def do_format(self, evaluation, format): 636 return self 637 638 def format(self, evaluation, fmt): 639 return self 640 641 def get_head(self): 642 return Symbol(self.get_name()) 643 644 @property 645 def head(self): 646 return self.get_head() 647 648 @head.setter 649 def head(self, value): 650 raise ValueError("BoxConstruct.head is write protected.") 651 652 @property 653 def leaves(self): 654 return self._leaves 655 656 @leaves.setter 657 def leaves(self, value): 658 raise ValueError("BoxConstruct.leaves is write protected.") 659 660 # I need to repeat this, because this is not 661 # an expression... 662 def has_form(self, heads, *leaf_counts): 663 """ 664 leaf_counts: 665 (,): no leaves allowed 666 (None,): no constraint on number of leaves 667 (n, None): leaf count >= n 668 (n1, n2, ...): leaf count in {n1, n2, ...} 669 """ 670 671 head_name = self.get_name() 672 if isinstance(heads, (tuple, list, set)): 673 if head_name not in [ensure_context(h) for h in heads]: 674 return False 675 else: 676 if head_name != ensure_context(heads): 677 return False 678 if not leaf_counts: 679 return False 680 if leaf_counts and leaf_counts[0] is not None: 681 count = len(self._leaves) 682 if count not in leaf_counts: 683 if ( 684 len(leaf_counts) == 2 685 and leaf_counts[1] is None # noqa 686 and count >= leaf_counts[0] 687 ): 688 return True 689 else: 690 return False 691 return True 692 693 def flatten_pattern_sequence(self, evaluation) -> "BoxConstruct": 694 return self 695 696 def get_option_values(self, leaves, **options): 697 evaluation = options.get("evaluation", None) 698 if evaluation: 699 default = evaluation.definitions.get_options(self.get_name()).copy() 700 options = Expression("List", *leaves).get_option_values(evaluation) 701 default.update(options) 702 else: 703 from mathics.core.parser import parse_builtin_rule 704 705 default = {} 706 for option, value in self.options.items(): 707 option = ensure_context(option) 708 default[option] = parse_builtin_rule(value) 709 return default 710 711 def boxes_to_text(self, leaves, **options) -> str: 712 raise BoxConstructError 713 714 def boxes_to_mathml(self, leaves, **options) -> str: 715 raise BoxConstructError 716 717 def boxes_to_tex(self, leaves, **options) -> str: 718 raise BoxConstructError 719 720 721class PatternError(Exception): 722 def __init__(self, name, tag, *args): 723 super().__init__() 724 self.name = name 725 self.tag = tag 726 self.args = args 727 728 729class PatternArgumentError(PatternError): 730 def __init__(self, name, count, expected): 731 super().__init__(name, "argr", count, expected) 732 733 734class PatternObject(InstanceableBuiltin, Pattern): 735 needs_verbatim = True 736 737 arg_counts: typing.List[int] = [] 738 739 def init(self, expr): 740 super().init(expr) 741 if self.arg_counts is not None: 742 if len(expr.leaves) not in self.arg_counts: 743 self.error_args(len(expr.leaves), *self.arg_counts) 744 self.expr = expr 745 self.head = Pattern.create(expr.head) 746 self.leaves = [Pattern.create(leaf) for leaf in expr.leaves] 747 748 def error(self, tag, *args): 749 raise PatternError(self.get_name(), tag, *args) 750 751 def error_args(self, count, *expected): 752 raise PatternArgumentError(self.get_name(), count, *expected) 753 754 def get_lookup_name(self) -> str: 755 return self.get_name() 756 757 def get_head_name(self) -> str: 758 return self.get_name() 759 760 def get_sort_key(self, pattern_sort=False): 761 return self.expr.get_sort_key(pattern_sort=pattern_sort) 762 763 def get_match_count(self, vars={}): 764 return (1, 1) 765 766 def get_match_candidates(self, leaves, expression, attributes, evaluation, vars={}): 767 return leaves 768 769 def get_attributes(self, definitions): 770 return self.head.get_attributes(definitions) 771 772 773class MessageException(Exception): 774 def __init__(self, *message): 775 self._message = message 776 777 def message(self, evaluation): 778 evaluation.message(*self._message) 779 780 781class NegativeIntegerException(Exception): 782 pass 783 784 785@total_ordering 786class CountableInteger: 787 """ 788 CountableInteger is an integer specifying a countable amount (including 789 zero) that can optionally be specified as an upper bound through UpTo[]. 790 """ 791 792 # currently MMA does not support UpTo[Infinity], but Infinity already shows 793 # up in UpTo's parameter error messages as supported option; it would make 794 # perfect sense. currently, we stick with MMA's current behaviour and set 795 # _support_infinity to False. 796 _finite: bool 797 _upper_limit: bool 798 _integer: typing.Union[str, int] 799 _support_infinity = False 800 801 def __init__(self, value="Infinity", upper_limit=True): 802 self._finite = value != "Infinity" 803 if self._finite: 804 assert isinstance(value, int) and value >= 0 805 self._integer = value 806 else: 807 assert upper_limit 808 self._integer = None 809 self._upper_limit = upper_limit 810 811 def is_upper_limit(self) -> bool: 812 return self._upper_limit 813 814 def get_int_value(self) -> int: 815 assert self._finite 816 return cast(int, self._integer) 817 818 def __eq__(self, other) -> bool: 819 if isinstance(other, CountableInteger): 820 if self._finite: 821 return other._finite and cast(int, self._integer) == other._integer 822 else: 823 return not other._finite 824 elif isinstance(other, int): 825 return self._finite and cast(int, self._integer) == other 826 else: 827 return False 828 829 def __lt__(self, other) -> bool: 830 if isinstance(other, CountableInteger): 831 if self._finite: 832 return other._finite and cast(int, self._integer) < cast( 833 int, other._integer 834 ) 835 else: 836 return False 837 elif isinstance(other, int): 838 return self._finite and cast(int, self._integer) < other 839 else: 840 return False 841 842 @staticmethod 843 def from_expression(expr): 844 """ 845 :param expr: expression from which to build a CountableInteger 846 :return: an instance of CountableInteger or None, if the whole 847 original expression should remain unevaluated. 848 :raises: MessageException, NegativeIntegerException 849 """ 850 851 if isinstance(expr, Integer): 852 py_n = expr.get_int_value() 853 if py_n >= 0: 854 return CountableInteger(py_n, upper_limit=False) 855 else: 856 raise NegativeIntegerException() 857 elif expr.get_head_name() == "System`UpTo": 858 if len(expr.leaves) != 1: 859 raise MessageException("UpTo", "argx", len(expr.leaves)) 860 else: 861 n = expr.leaves[0] 862 if isinstance(n, Integer): 863 py_n = n.get_int_value() 864 if py_n < 0: 865 raise MessageException("UpTo", "innf", expr) 866 else: 867 return CountableInteger(py_n, upper_limit=True) 868 elif CountableInteger._support_infinity: 869 if ( 870 n.get_head_name() == "System`DirectedInfinity" 871 and len(n.leaves) == 1 872 ): 873 if n.leaves[0].get_int_value() > 0: 874 return CountableInteger("Infinity", upper_limit=True) 875 else: 876 return CountableInteger(0, upper_limit=True) 877 878 return None # leave original expression unevaluated 879