1# -*- coding: utf-8 -*-
2
3"""
4Options and Default Arguments
5"""
6
7from mathics.version import __version__  # noqa used in loading to check consistency.
8
9from mathics.builtin.base import Builtin, Test, get_option
10from mathics.core.expression import (
11    Symbol,
12    String,
13    Expression,
14    get_default_value,
15    ensure_context,
16    strip_context,
17)
18from mathics.builtin.drawing.image import Image
19
20
21class Options(Builtin):
22    """
23    <dl>
24    <dt>'Options[$f$]'
25        <dd>gives a list of optional arguments to $f$ and their
26        default values.
27    </dl>
28
29    You can assign values to 'Options' to specify options.
30    >> Options[f] = {n -> 2}
31     = {n -> 2}
32    >> Options[f]
33     = {n :> 2}
34    >> f[x_, OptionsPattern[f]] := x ^ OptionValue[n]
35    >> f[x]
36     = x ^ 2
37    >> f[x, n -> 3]
38     = x ^ 3
39
40    #> f[x_, OptionsPattern[f]] := x ^ OptionValue["m"];
41    #> Options[f] = {"m" -> 7};
42    #> f[x]
43     = x ^ 7
44
45    Delayed option rules are evaluated just when the corresponding 'OptionValue' is called:
46    >> f[a :> Print["value"]] /. f[OptionsPattern[{}]] :> (OptionValue[a]; Print["between"]; OptionValue[a]);
47     | value
48     | between
49     | value
50    In contrast to that, normal option rules are evaluated immediately:
51    >> f[a -> Print["value"]] /. f[OptionsPattern[{}]] :> (OptionValue[a]; Print["between"]; OptionValue[a]);
52     | value
53     | between
54
55    Options must be rules or delayed rules:
56    >> Options[f] = {a}
57     : {a} is not a valid list of option rules.
58     = {a}
59    A single rule need not be given inside a list:
60    >> Options[f] = a -> b
61     = a -> b
62    >> Options[f]
63     = {a :> b}
64    Options can only be assigned to symbols:
65    >> Options[a + b] = {a -> b}
66     : Argument a + b at position 1 is expected to be a symbol.
67     = {a -> b}
68
69    #> f /: Options[f] = {a -> b}
70     = {a -> b}
71    #> Options[f]
72     = {a :> b}
73    #> f /: Options[g] := {a -> b}
74     : Rule for Options can only be attached to g.
75     = $Failed
76
77    #> Options[f] = a /; True
78     : a /; True is not a valid list of option rules.
79     = a /; True
80    """
81
82    def apply(self, f, evaluation):
83        "Options[f_]"
84
85        name = f.get_name()
86        if not name:
87            if isinstance(f, Image):
88                # FIXME ColorSpace, MetaInformation
89                options = f.metadata
90            else:
91                evaluation.message("Options", "sym", f, 1)
92                return
93        else:
94            options = evaluation.definitions.get_options(name)
95        result = []
96        for option, value in sorted(options.items(), key=lambda item: item[0]):
97            # Don't use HoldPattern, since the returned List should be
98            # assignable to Options again!
99            result.append(Expression("RuleDelayed", Symbol(option), value))
100        return Expression("List", *result)
101
102
103class OptionValue(Builtin):
104    """
105    <dl>
106    <dt>'OptionValue[$name$]'
107        <dd>gives the value of the option $name$ as specified in a
108        call to a function with 'OptionsPattern'.
109    <dt>'OptionValue[$f$, $name$]'
110        <dd>recover the value of the option $name$ associated to the symbol $f$.
111    <dt>'OptionValue[$f$, $optvals$, $name$]'
112        <dd>recover the value of the option $name$ associated to the symbol $f$,
113            extracting the values from $optvals$ if available.
114    <dt>'OptionValue[$\\ldots$, $list$]'
115        <dd>recover the value of the options in $list$ .
116    </dl>
117
118    >> f[a->3] /. f[OptionsPattern[{}]] -> {OptionValue[a]}
119     = {3}
120
121    Unavailable options generate a message:
122    >> f[a->3] /. f[OptionsPattern[{}]] -> {OptionValue[b]}
123     : Option name b not found.
124     = {b}
125
126    The argument of 'OptionValue' must be a symbol:
127    >> f[a->3] /. f[OptionsPattern[{}]] -> {OptionValue[a+b]}
128     : Argument a + b at position 1 is expected to be a symbol.
129     = {OptionValue[a + b]}
130    However, it can be evaluated dynamically:
131    >> f[a->5] /. f[OptionsPattern[{}]] -> {OptionValue[Symbol["a"]]}
132     = {5}
133    """
134
135    messages = {
136        "optnf": "Option name `1` not found.",
137    }
138
139    rules = {
140        "OptionValue[optnames_List]": "OptionValue/@optnames",
141        "OptionValue[f_, optnames_List]": "OptionValue[f,#1]&/@optnames",
142        "OptionValue[f_, opts_, optnames_List]": "OptionValue[f,opts, #1]&/@optnames",
143    }
144
145    def apply_1(self, optname, evaluation):
146        "OptionValue[optname_]"
147        if evaluation.options is None:
148            return
149
150        if type(optname) is String:
151            name = optname.to_python()[1:-1]
152        else:
153            name = optname.get_name()
154
155        name = optname.get_name()
156        if not name:
157            name = optname.get_string_value()
158            if name:
159                name = ensure_context(name)
160        if not name:
161            evaluation.message("OptionValue", "sym", optname, 1)
162            return
163
164        val = get_option(evaluation.options, name, evaluation)
165        if val is None:
166            evaluation.message("OptionValue", "optnf", optname)
167            return Symbol(name)
168        return val
169
170    def apply_2(self, f, optname, evaluation):
171        "OptionValue[f_, optname_]"
172        return self.apply_3(f, None, optname, evaluation)
173
174    def apply_3(self, f, optvals, optname, evaluation):
175        "OptionValue[f_, optvals_, optname_]"
176        if type(optname) is String:
177            name = optname.to_python()[1:-1]
178        else:
179            name = optname.get_name()
180
181        if not name:
182            name = optname.get_string_value()
183            if name:
184                name = ensure_context(name)
185        if not name:
186            evaluation.message("OptionValue", "sym", optname, 1)
187            return
188        # Look first in the explicit list
189        if optvals:
190            val = get_option(optvals.get_option_values(evaluation), name, evaluation)
191        else:
192            val = None
193        # then, if not found, look at $f$. It could be a symbol, or a list of symbols, rules, and list of rules...
194        if val is None:
195            if f.is_symbol():
196                val = get_option(
197                    evaluation.definitions.get_options(f.get_name()), name, evaluation
198                )
199            else:
200                if f.get_head_name() in ("System`Rule", "System`RuleDelayed"):
201                    f = Expression("List", f)
202                if f.get_head_name() == "System`List":
203                    for leave in f.get_leaves():
204                        if leave.is_symbol():
205                            val = get_option(
206                                evaluation.definitions.get_options(leave.get_name()),
207                                name,
208                                evaluation,
209                            )
210                            if val:
211                                break
212                        else:
213                            values = leave.get_option_values(evaluation)
214                            val = get_option(values, name, evaluation)
215                            if val:
216                                break
217
218        if val is None and evaluation.options:
219            val = get_option(evaluation.options, name, evaluation)
220        if val is None:
221            evaluation.message("OptionValue", "optnf", optname)
222            return Symbol(name)
223        return val
224
225
226class Default(Builtin):
227    """
228    <dl>
229    <dt>'Default[$f$]'
230        <dd>gives the default value for an omitted paramter of $f$.
231    <dt>'Default[$f$, $k$]'
232        <dd>gives the default value for a parameter on the $k$th position.
233    <dt>'Default[$f$, $k$, $n$]'
234        <dd>gives the default value for the $k$th parameter out of $n$.
235    </dl>
236
237    Assign values to 'Default' to specify default values.
238
239    >> Default[f] = 1
240     = 1
241    >> f[x_.] := x ^ 2
242    >> f[]
243     = 1
244
245    Default values are stored in 'DefaultValues':
246    >> DefaultValues[f]
247     = {HoldPattern[Default[f]] :> 1}
248
249    You can use patterns for $k$ and $n$:
250    >> Default[h, k_, n_] := {k, n}
251    Note that the position of a parameter is relative to the pattern, not the matching expression:
252    >> h[] /. h[___, ___, x_., y_., ___] -> {x, y}
253     = {{3, 5}, {4, 5}}
254    """
255
256    def apply(self, f, i, evaluation):
257        "Default[f_, i___]"
258
259        i = i.get_sequence()
260        if len(i) > 2:
261            evaluation.message("Default", "argb", 1 + len(i), 1, 3)
262            return
263        i = [index.get_int_value() for index in i]
264        for index in i:
265            if index is None or index < 1:
266                evaluation.message("Default", "intp")
267                return
268        name = f.get_name()
269        if not name:
270            evaluation.message("Default", "sym", f, 1)
271            return
272        result = get_default_value(name, evaluation, *i)
273        return result
274
275
276class OptionQ(Test):
277    """
278    <dl>
279    <dt>'OptionQ[$expr$]'
280        <dd>returns 'True' if $expr$ has the form of a valid option
281        specification.
282    </dl>
283
284    Examples of option specifications:
285    >> OptionQ[a -> True]
286     = True
287    >> OptionQ[a :> True]
288     = True
289    >> OptionQ[{a -> True}]
290     = True
291    >> OptionQ[{a :> True}]
292     = True
293
294    Options lists are flattened when are applyied, so
295    >> OptionQ[{a -> True, {b->1, "c"->2}}]
296     = True
297    >> OptionQ[{a -> True, {b->1, c}}]
298     = False
299    >> OptionQ[{a -> True, F[b->1,c->2]}]
300     = False
301
302    'OptionQ' returns 'False' if its argument is not a valid option
303    specification:
304    >> OptionQ[x]
305     = False
306    """
307
308    def test(self, expr):
309        expr = expr.flatten(Symbol("List"))
310        if not expr.has_form("List", None):
311            expr = [expr]
312        else:
313            expr = expr.get_leaves()
314        return all(
315            e.has_form("Rule", None) or e.has_form("RuleDelayed", 2) for e in expr
316        )
317
318
319class NotOptionQ(Test):
320    """
321    <dl>
322    <dt>'NotOptionQ[$expr$]'
323        <dd>returns 'True' if $expr$ does not have the form of a valid
324        option specification.
325    </dl>
326
327    >> NotOptionQ[x]
328     = True
329    >> NotOptionQ[2]
330     = True
331    >> NotOptionQ["abc"]
332     = True
333
334    >> NotOptionQ[a -> True]
335     = False
336    """
337
338    def test(self, expr):
339        expr = expr.flatten(Symbol("List"))
340        if not expr.has_form("List", None):
341            expr = [expr]
342        else:
343            expr = expr.get_leaves()
344        return not all(
345            e.has_form("Rule", None) or e.has_form("RuleDelayed", 2) for e in expr
346        )
347
348
349class FilterRules(Builtin):
350    """
351    <dl>
352    <dt>'FilterRules[$rules$, $pattern$]'
353        <dd>gives those $rules$ that have a left side that matches $pattern$.
354    <dt>'FilterRules[$rules$, {$pattern1$, $pattern2$, ...}]'
355        <dd>gives those $rules$ that have a left side that match at least one of $pattern1$, $pattern2$, ...
356    </dl>
357
358    >> FilterRules[{x -> 100, y -> 1000}, x]
359     = {x -> 100}
360
361    >> FilterRules[{x -> 100, y -> 1000, z -> 10000}, {a, b, x, z}]
362     = {x -> 100, z -> 10000}
363    """
364
365    rules = {
366        "FilterRules[rules_List, patterns_List]": "FilterRules[rules, Alternatives @@ patterns]",
367    }
368
369    def apply(self, rules, pattern, evaluation):
370        "FilterRules[rules_List, pattern_]"
371        from mathics.builtin.patterns import Matcher
372
373        match = Matcher(pattern).match
374
375        def matched():
376            for rule in rules.leaves:
377                if rule.has_form("Rule", 2) and match(rule.leaves[0], evaluation):
378                    yield rule
379
380        return Expression("List", *list(matched()))
381
382
383def options_to_rules(options, filter=None):
384    items = sorted(options.items())
385    if filter:
386        items = [
387            (name, value)
388            for name, value in items
389            if strip_context(name) in filter.keys()
390        ]
391    return [Expression("Rule", Symbol(name), value) for name, value in items]
392