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