diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index c0c1aa858..b8fe6b54b 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -13,7 +13,7 @@ import sys import time from datetime import datetime, timedelta -from typing import Optional +from typing import Optional, Union import dateutil.parser @@ -97,16 +97,7 @@ EPOCH_START = datetime(1900, 1, 1) -if not hasattr(timedelta, "total_seconds"): - - def total_seconds(td): - return ( - float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) - / 10**6 - ) - -else: - total_seconds = timedelta.total_seconds +total_seconds = timedelta.total_seconds SymbolDateObject = Symbol("DateObject") SymbolDateString = Symbol("DateString") @@ -114,8 +105,10 @@ def total_seconds(td): class _Date: - def __init__(self, datelist=[], absolute=None, datestr=None): - datelist += [1900, 1, 1, 0, 0, 0.0][len(datelist) :] + def __init__( + self, datelist_arg: Union[list, tuple] = [], absolute=None, datestr=None + ): + datelist = list(datelist_arg) + [1900, 1, 1, 0, 0, 0.0][len(datelist_arg) :] self.date = datetime( datelist[0], datelist[1], @@ -249,7 +242,7 @@ def to_datelist(self, epochtime, evaluation: Evaluation): ] return datelist - if not isinstance(etime, list): + if not isinstance(etime, (list, tuple)): evaluation.message(form_name, "arg", etime) return @@ -258,7 +251,7 @@ def to_datelist(self, epochtime, evaluation: Evaluation): for i, val in enumerate(etime) ): default_date = [1900, 1, 1, 0, 0, 0.0] - datelist = etime + default_date[len(etime) :] + datelist = list(etime) + default_date[len(etime) :] prec_part, imprec_part = datelist[:2], datelist[2:] try: @@ -289,12 +282,16 @@ def to_datelist(self, epochtime, evaluation: Evaluation): if len(etime) == 2: if ( isinstance(etime[0], str) - and isinstance(etime[1], list) # noqa + and isinstance(etime[1], (list, tuple)) # noqa and all(isinstance(s, str) for s in etime[1]) ): is_spec = [ str(s).strip('"') in DATE_STRING_FORMATS.keys() for s in etime[1] ] + + if isinstance(etime, tuple): + etime = list(etime) + etime[1] = [str(s).strip('"') for s in etime[1]] if sum(is_spec) == len(is_spec): @@ -389,7 +386,7 @@ def eval_spec(self, epochtime, evaluation: Evaluation) -> Optional[MachineReal]: if datelist is None: return - date = _Date(datelist=datelist) + date = _Date(datelist_arg=datelist) tdelta = date.date - EPOCH_START if tdelta.microseconds == 0: return Integer(int(total_seconds(tdelta))) @@ -487,8 +484,8 @@ def eval( # Process dates pydate1, pydate2 = date1.to_python(), date2.to_python() - if isinstance(pydate1, list): # Date List - idate = _Date(datelist=pydate1) + if isinstance(pydate1, (list, tuple)): # Date List + idate = _Date(datelist_arg=pydate1) elif isinstance(pydate1, (float, int)): # Absolute Time idate = _Date(absolute=pydate1) elif isinstance(pydate1, str): # Date string @@ -497,8 +494,8 @@ def eval( evaluation.message("DateDifference", "date", date1) return - if isinstance(pydate2, list): # Date List - fdate = _Date(datelist=pydate2) + if isinstance(pydate2, (list, tuple)): # Date List + fdate = _Date(datelist_arg=pydate2) elif isinstance(pydate2, (int, float)): # Absolute Time fdate = _Date(absolute=pydate2) elif isinstance(pydate1, str): # Date string @@ -517,7 +514,9 @@ def eval( pyunits = units.to_python() if isinstance(pyunits, str): pyunits = [str(pyunits.strip('"'))] - elif isinstance(pyunits, list) and all(isinstance(p, str) for p in pyunits): + elif isinstance(pyunits, (list, tuple)) and all( + isinstance(p, str) for p in pyunits + ): pyunits = [p.strip('"') for p in pyunits] if not all(p in TIME_INCREMENTS.keys() for p in pyunits): @@ -762,9 +761,9 @@ def eval( # Process date pydate = date.to_python() - if isinstance(pydate, list): + if isinstance(pydate, (list, tuple)): date_prec = len(pydate) - idate = _Date(datelist=pydate) + idate = _Date(datelist_arg=pydate) elif isinstance(pydate, float) or isinstance(pydate, int): date_prec = "absolute" idate = _Date(absolute=pydate) @@ -779,13 +778,17 @@ def eval( pyoff = off.to_python() if isinstance(pyoff, float) or isinstance(pyoff, int): pyoff = [[pyoff, '"Day"']] - elif isinstance(pyoff, list) and len(pyoff) == 2 and isinstance(pyoff[1], str): + elif ( + isinstance(pyoff, (list, tuple)) + and len(pyoff) == 2 + and isinstance(pyoff[1], str) + ): pyoff = [pyoff] # Strip " marks pyoff = [[x[0], x[1].strip('"')] for x in pyoff] - if isinstance(pyoff, list) and all( # noqa + if isinstance(pyoff, (list, tuple)) and all( # noqa len(o) == 2 and o[1] in TIME_INCREMENTS.keys() and isinstance(o[0], (float, int)) @@ -940,15 +943,16 @@ def eval( self, epochtime: BaseElement, form: BaseElement, evaluation: Evaluation ) -> Optional[String]: "DateString[epochtime_, form_]" + datelist = self.to_datelist(epochtime, evaluation) if datelist is None: return - date = _Date(datelist=datelist) + date = _Date(datelist_arg=datelist) pyform = form.to_python() - if not isinstance(pyform, list): + if not isinstance(pyform, (list, tuple)): pyform = [pyform] pyform = [x.strip('"') for x in pyform] diff --git a/mathics/builtin/directories/directory_names.py b/mathics/builtin/directories/directory_names.py index d6f57ccb4..b7204a349 100644 --- a/mathics/builtin/directories/directory_names.py +++ b/mathics/builtin/directories/directory_names.py @@ -177,14 +177,19 @@ class FileNameJoin(Builtin): def eval(self, pathlist, evaluation: Evaluation, options: dict): "FileNameJoin[pathlist_List, OptionsPattern[FileNameJoin]]" + # Convert pathlist to a Python list, and strip leading and trailing + # quotes if that appears. py_pathlist = pathlist.to_python() - if not all(isinstance(p, str) and p[0] == p[-1] == '"' for p in py_pathlist): + + if not all(isinstance(p, str) for p in py_pathlist): return - py_pathlist = [p[1:-1] for p in py_pathlist] - operating_system = ( - options["System`OperatingSystem"].evaluate(evaluation).get_string_value() - ) + if isinstance(py_pathlist, tuple): + py_pathlist = list(py_pathlist) + else: + py_pathlist = [p[1:-1] if p[0] == p[-1] == '"' else p for p in py_pathlist] + + operating_system = options["System`OperatingSystem"].evaluate(evaluation).value if operating_system not in ["MacOSX", "Windows", "Unix"]: evaluation.message( diff --git a/mathics/builtin/file_operations/file_properties.py b/mathics/builtin/file_operations/file_properties.py index 322970c19..74c340704 100644 --- a/mathics/builtin/file_operations/file_properties.py +++ b/mathics/builtin/file_operations/file_properties.py @@ -4,21 +4,18 @@ import os import os.path as osp -import time +from datetime import datetime from mathics.builtin.exp_structure.size_and_sig import Hash from mathics.builtin.files_io.files import MathicsOpen -from mathics.core.atoms import Real, String +from mathics.core.atoms import String from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin, MessageException from mathics.core.convert.expression import to_expression -from mathics.core.convert.python import from_python from mathics.core.evaluation import Evaluation -from mathics.core.expression import Expression from mathics.core.streams import path_search from mathics.core.symbols import Symbol, SymbolNull -from mathics.core.systemsymbols import SymbolAbsoluteTime, SymbolFailed, SymbolNone -from mathics.eval.nevaluator import eval_N +from mathics.core.systemsymbols import SymbolFailed, SymbolNone sort_order = "mathics.builtin.file-operations.file_properties" @@ -103,17 +100,17 @@ def eval(self, path, timetype, evaluation): evaluation.message("FileDate", "datetype") return - # Offset for system epoch - epochtime_expr = Expression( - SymbolAbsoluteTime, String(time.strftime("%Y-%m-%d %H:%M", time.gmtime(0))) + dt_object = datetime.fromtimestamp(result) + # Extract the year, month, day, hour, and minute into a tuple + datetime_tuple = ( + dt_object.year, + dt_object.month, + dt_object.day, + dt_object.hour, + dt_object.minute, + dt_object.second, ) - epochtime_N = eval_N(epochtime_expr, evaluation) - if epochtime_N is None: - return None - epochtime = epochtime_N.to_python() - result += epochtime - - return to_expression("DateList", Real(result)) + return to_expression("DateList", datetime_tuple) def eval_default(self, path, evaluation): "FileDate[path_]" @@ -299,7 +296,7 @@ def eval(self, filename, datelist, attribute, evaluation): # Check datelist if not ( - isinstance(py_datelist, list) + isinstance(py_datelist, (list, tuple)) and len(py_datelist) == 6 and all(isinstance(d, int) for d in py_datelist[:-1]) and isinstance(py_datelist[-1], float) @@ -311,25 +308,13 @@ def eval(self, filename, datelist, attribute, evaluation): evaluation.message("SetFileDate", "datetype") return - epochtime = ( - to_expression( - "AbsoluteTime", time.strftime("%Y-%m-%d %H:%M", time.gmtime(0)) - ) - .evaluate(evaluation) - .to_python() - ) - - stattime = to_expression("AbsoluteTime", from_python(py_datelist)) - stattime_N = eval_N(stattime, evaluation) - if stattime_N is None: - return - - stattime = stattime_N.to_python() - epochtime + file_timestamp = datetime(*py_datelist[:4]).timestamp() try: os.stat(py_filename) if py_attr == '"Access"': - os.utime(py_filename, (stattime, osp.getatime(py_filename))) + os.utime(py_filename, (file_timestamp, osp.getatime(py_filename))) + return SymbolNull if py_attr == '"Creation"': if os.name == "posix": evaluation.message("SetFileDate", "nocreationunix") @@ -338,9 +323,9 @@ def eval(self, filename, datelist, attribute, evaluation): # TODO: Note: This is windows only return SymbolFailed if py_attr == '"Modification"': - os.utime(py_filename, (osp.getatime(py_filename), stattime)) - if py_attr == "All": - os.utime(py_filename, (stattime, stattime)) + os.utime(py_filename, (osp.getatime(py_filename), file_timestamp)) + elif py_attr == "All": + os.utime(py_filename, (file_timestamp, file_timestamp)) except OSError: # evaluation.message(...) return SymbolFailed diff --git a/mathics/builtin/files_io/files.py b/mathics/builtin/files_io/files.py index 86fdc04e8..a14da432d 100644 --- a/mathics/builtin/files_io/files.py +++ b/mathics/builtin/files_io/files.py @@ -267,9 +267,8 @@ class FilePrint(Builtin): """ messages = { - "fstr": ( - "File specification `1` is not a string of " "one or more characters." - ), + "zstr": ("The file name cannot be an empty string."), + "badfile": ("The specified argument, `1`, should be a valid string."), } options = { @@ -281,33 +280,35 @@ class FilePrint(Builtin): def eval(self, path, evaluation: Evaluation, options: dict): "FilePrint[path_, OptionsPattern[FilePrint]]" + + if not isinstance(path, String): + evaluation.message("FilePrint", "badfile", path) + return + pypath = path.to_python() + if not ( isinstance(pypath, str) and pypath[0] == pypath[-1] == '"' and len(pypath) > 2 ): - evaluation.message("FilePrint", "fstr", path) + evaluation.message("FilePrint", "zstr", path) return - pypath, _ = path_search(pypath[1:-1]) + resolved_pypath, _ = path_search(pypath[1:-1]) # Options record_separators = options["System`RecordSeparators"].to_python() - assert isinstance(record_separators, list) - assert all( - isinstance(s, str) and s[0] == s[-1] == '"' for s in record_separators - ) - record_separators = [s[1:-1] for s in record_separators] + assert isinstance(record_separators, tuple) - if pypath is None: + if resolved_pypath is None: evaluation.message("General", "noopen", path) return - if not osp.isfile(pypath): + if not osp.isfile(resolved_pypath): return SymbolFailed try: - with MathicsOpen(pypath, "r") as f: + with MathicsOpen(resolved_pypath, "r") as f: result = f.read() except IOError: evaluation.message("General", "noopen", path) @@ -1374,18 +1375,26 @@ def eval(self, name, n, text, evaluation: Evaluation, options: dict): stream = to_expression("InputStream", name, n) - if not isinstance(py_text, list): + if not isinstance(py_text, (list, tuple)): py_text = [py_text] - if not all(isinstance(t, str) and t[0] == t[-1] == '"' for t in py_text): + if not all(isinstance(t, str) for t in py_text): evaluation.message("Find", "unknown", to_expression("Find", stream, text)) return - py_text = [t[1:-1] for t in py_text] + # If py_text comes from a (literal) value, then there are no + # leading/trailing quotes around strings. If it is still + # possible that py_text can be a list, then there could be + # leading/traling quotes. + if isinstance(py_text, list): + py_text = [t[1:-1] if t[0] == t[-1] == '"' else t for t in py_text] while True: tmp = super(Find, self).eval(stream, Symbol("Record"), evaluation, options) - py_tmp = tmp.to_python()[1:-1] + if not isinstance(tmp, String): + return SymbolFailed + + py_tmp = tmp.value if py_tmp == "System`EndOfFile": evaluation.message( diff --git a/mathics/builtin/files_io/filesystem.py b/mathics/builtin/files_io/filesystem.py index ffe2ebe5a..503becbcf 100644 --- a/mathics/builtin/files_io/filesystem.py +++ b/mathics/builtin/files_io/filesystem.py @@ -66,10 +66,12 @@ def eval(self, name, evaluation): py_name = name.to_python() - if not (isinstance(py_name, str) and py_name[0] == py_name[-1] == '"'): + if not isinstance(py_name, str): evaluation.message("AbsoluteFileName", "fstr", name) return - py_name = py_name[1:-1] + + if py_name[0] == py_name[-1] == '"': + py_name = py_name[1:-1] result, _ = path_search(py_name) @@ -164,15 +166,18 @@ def eval(self, source, dest, evaluation): py_dest = dest.to_python() # Check filenames - if not (isinstance(py_source, str) and py_source[0] == py_source[-1] == '"'): + if not (isinstance(py_source, str)): evaluation.message("CopyFile", "fstr", source) return - if not (isinstance(py_dest, str) and py_dest[0] == py_dest[-1] == '"'): + if not (isinstance(py_dest, str)): evaluation.message("CopyFile", "fstr", dest) return - py_source = py_source[1:-1] - py_dest = py_dest[1:-1] + if py_source[0] == py_source[-1] == '"': + py_source = py_source[1:-1] + + if py_dest[0] == py_dest[-1] == '"': + py_dest = py_dest[1:-1] py_source, _ = path_search(py_source) @@ -290,13 +295,13 @@ def eval(self, filename, evaluation): "DeleteFile[filename_]" py_path = filename.to_python() - if not isinstance(py_path, list): + if not isinstance(py_path, (list, tuple)): py_path = [py_path] py_paths = [] for path in py_path: # Check filenames - if not (isinstance(path, str) and path[0] == path[-1] == '"'): + if not isinstance(path, str): evaluation.message( "DeleteFile", "strs", @@ -305,7 +310,8 @@ def eval(self, filename, evaluation): ) return - path = path[1:-1] + if path[0] == path[-1] == '"': + path = path[1:-1] path, _ = path_search(path) if path is None: diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index a01bb046f..021440fc4 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -508,7 +508,7 @@ class PythonForm(FormBaseClass): >> E // PythonForm = sympy.E >> {1, 2, 3} // PythonForm - = [1, 2, 3] + = (1, 2, 3) """ in_outputforms = True diff --git a/mathics/builtin/list/rearrange.py b/mathics/builtin/list/rearrange.py index 314e7b8f6..34aa11475 100644 --- a/mathics/builtin/list/rearrange.py +++ b/mathics/builtin/list/rearrange.py @@ -682,19 +682,21 @@ def eval_list(self, expr, n, h, evaluation): # prepare levels # find max depth which matches `h` expr, max_depth = walk_levels(expr) - max_depth = {"max_depth": max_depth} # hack to modify max_depth from callback + max_depth_dict = { + "max_depth": max_depth + } # hack to modify max_depth from callback def callback(expr, pos): - if len(pos) < max_depth["max_depth"] and ( + if len(pos) < max_depth_dict["max_depth"] and ( isinstance(expr, Atom) or expr.head != h ): - max_depth["max_depth"] = len(pos) + max_depth_dict["max_depth"] = len(pos) return expr - expr, depth = walk_levels(expr, callback=callback, include_pos=True, start=0) - max_depth = max_depth["max_depth"] + expr, _ = walk_levels(expr, callback=callback, include_pos=True, start=0) + max_depth = max_depth_dict["max_depth"] - levels = n.to_python() + levels = list(n.to_python()) # mappings if isinstance(levels, list) and all(isinstance(level, int) for level in levels): @@ -706,7 +708,7 @@ def callback(expr, pos): return seen_levels = [] for level in levels: - if not (isinstance(level, list) and len(level) > 0): + if not (isinstance(level, (list, tuple)) and len(level) > 0): evaluation.message("Flatten", "flpi", n) return for r in level: diff --git a/mathics/builtin/numbers/numbertheory.py b/mathics/builtin/numbers/numbertheory.py index 903a2e0c7..98678f6b4 100644 --- a/mathics/builtin/numbers/numbertheory.py +++ b/mathics/builtin/numbers/numbertheory.py @@ -1062,7 +1062,7 @@ def eval(self, interval, n, evaluation: Evaluation): py_n = n.to_python() py_int = interval.to_python() - if not (isinstance(py_int, list) and len(py_int) == 2): + if not (isinstance(py_int, (list, tuple)) and len(py_int) == 2): evaluation.message("RandomPrime", "prmrng", interval) imin, imax = min(py_int), max(py_int) diff --git a/mathics/builtin/numbers/randomnumbers.py b/mathics/builtin/numbers/randomnumbers.py index 6789ac407..8355df9de 100644 --- a/mathics/builtin/numbers/randomnumbers.py +++ b/mathics/builtin/numbers/randomnumbers.py @@ -424,7 +424,7 @@ def eval_list(self, zmin, zmax, ns, evaluation): return py_ns = ns.to_python() - if not isinstance(py_ns, list): + if not isinstance(py_ns, (list, tuple)): py_ns = [py_ns] if not all(isinstance(i, int) and i >= 0 for i in py_ns): diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index d6e542a60..8987c35b9 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -63,9 +63,21 @@ def __getnewargs__(self): """ return (self._value,) + def __eq__(self, other): + if isinstance(other, Number): + return self.get_sort_key() == other.get_sort_key() + else: + return False + def __str__(self) -> str: return str(self.value) + def default_format(self, evaluation, form) -> str: + return str(self.value) + + def do_copy(self) -> "Number": + raise NotImplementedError + # FIXME: can we refactor or subclass objects to remove pattern_sort? def get_sort_key(self, pattern_sort=False) -> tuple: """ @@ -109,21 +121,15 @@ def to_mpmath(self, precision: Optional[int] = None) -> mpmath.ctx_mp_python.mpf return mpmath.mpf(self.value) return mpmath.mpf(self.value) - @property - def value(self) -> T: + def to_python(self, *_, **kwargs): + """Returns a native builtin Python object + something in (int, float, complex, str, tuple, list or dict.). + (See discussions in + https://github.com/Mathics3/mathics-core/discussions/550 + and + https://github.com/Mathics3/mathics-core/pull/551 """ - Returns this number's value. - """ - return self._value - - def __eq__(self, other): - if isinstance(other, Number): - return self.get_sort_key() == other.get_sort_key() - else: - return False - - def default_format(self, evaluation, form) -> str: - return str(self.value) + return self.value def round(self, d: Optional[int] = None) -> "Number": """ @@ -131,8 +137,12 @@ def round(self, d: Optional[int] = None) -> "Number": """ return self - def do_copy(self) -> "Number": - raise NotImplementedError + @property + def value(self) -> T: + """ + Equivalent value in Python's native datatype. + """ + return self._value def _ExponentFunction(value): @@ -256,12 +266,24 @@ def __ne__(self, other) -> bool: else super().__ne__(other) ) + def __neg__(self) -> "Integer": + return Integer(-self._value) + def abs(self) -> "Integer": return -self if self < Integer0 else self def atom_to_boxes(self, f, evaluation): return self.make_boxes(f.get_name()) + def get_int_value(self) -> int: + return self._value + + @property + def is_zero(self) -> bool: + # Note: 0 is self._value or the other way around is a syntax + # error. + return self._value == 0 + def make_boxes(self, form) -> "String": from mathics.eval.makeboxes import _boxed_string @@ -281,12 +303,6 @@ def make_boxes(self, form) -> "String": return int_to_string_shorter_repr(self._value, form) - def to_sympy(self, **kwargs): - return sympy.Integer(self._value) - - def to_python(self, *args, **kwargs): - return self.value - def round(self, d: Optional[int] = None) -> Union["MachineReal", "PrecisionReal"]: """ Produce a Real approximation of ``self`` with decimal precision ``d``. @@ -302,8 +318,8 @@ def round(self, d: Optional[int] = None) -> Union["MachineReal", "PrecisionReal" d = MACHINE_PRECISION_VALUE return PrecisionReal(sympy.Float(self.value, d)) - def get_int_value(self) -> int: - return self._value + def to_sympy(self, **_): + return sympy.Integer(self._value) def sameQ(self, other) -> bool: """Mathics SameQ""" @@ -315,15 +331,6 @@ def do_copy(self) -> "Integer": def user_hash(self, update): update(b"System`Integer>" + str(self._value).encode("utf8")) - def __neg__(self) -> "Integer": - return Integer(-self._value) - - @property - def is_zero(self) -> bool: - # Note: 0 is self._value or the other way around is a syntax - # error. - return self._value == 0 - Integer0 = Integer(0) Integer1 = Integer(1) diff --git a/mathics/core/convert/expression.py b/mathics/core/convert/expression.py index 22963851a..60784bfea 100644 --- a/mathics/core/convert/expression.py +++ b/mathics/core/convert/expression.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from typing import Any, Callable, Type, Union +from typing import Any, Callable, Union from mathics.core.convert.python import from_python from mathics.core.element import BaseElement @@ -52,7 +52,7 @@ def to_expression( def to_expression_with_specialization( head: BaseElement, *elements: Any, - elements_conversion_fn: Callable = from_python, + _: Callable = from_python, ) -> Union[ListExpression, Expression]: """ This expression constructor will figure out what the right kind of @@ -77,11 +77,13 @@ def to_mathics_list( to_mathics_list(1, 2, 3) to_mathics_list(1, 2, 3, elements_conversion_fn=Integer) """ - elements_tuple, elements_properties, _ = convert_expression_elements( + elements_tuple, elements_properties, literal_values = convert_expression_elements( elements, elements_conversion_fn ) list_expression = ListExpression( - *elements_tuple, elements_properties=elements_properties + *elements_tuple, + elements_properties=elements_properties, + literal_values=literal_values, ) return list_expression @@ -102,5 +104,5 @@ def to_numeric_args(mathics_args: BaseElement, evaluation) -> tuple: expression_constructor_map = { - SymbolList: lambda head, *args, **kwargs: ListExpression(*args, **kwargs) + SymbolList: lambda _, *args, **kwargs: ListExpression(*args, **kwargs) } diff --git a/mathics/core/element.py b/mathics/core/element.py index 1b4971bb3..e020e99bf 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -431,13 +431,13 @@ def user_hash(self, update) -> None: raise NotImplementedError def to_python(self, *args, **kwargs): - # Returns a native builtin Python object - # something in (int, float, complex, str, tuple, list or dict.). - # (See discussions in - # https://github.com/Mathics3/mathics-core/discussions/550 - # and - # https://github.com/Mathics3/mathics-core/pull/551 - # + """Returns a native builtin Python object + something in (int, float, complex, str, tuple, list or dict.). + (See discussions in + https://github.com/Mathics3/mathics-core/discussions/550 + and + https://github.com/Mathics3/mathics-core/pull/551 + """ raise NotImplementedError def to_mpmath(self): diff --git a/mathics/core/expression.py b/mathics/core/expression.py index b77d140dd..65569ccf3 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -353,9 +353,7 @@ def _build_elements_properties(self): values = [] for element in self._elements: # Test for the literalness, and the three properties mentioned above - if element.is_literal: - values.append(element.value) - else: + if not element.is_literal: self.elements_properties.elements_fully_evaluated = False if isinstance(element, Expression): @@ -1547,7 +1545,7 @@ def slice(self, head, py_slice, evaluation): def to_mpmath(self): return None - def to_python(self, *args, **kwargs): + def to_python(self, *args, **kwargs) -> Any: """ Convert the Expression to a Python object: List[...] -> Python list @@ -1563,6 +1561,13 @@ def to_python(self, *args, **kwargs): """ from mathics.core.builtin import mathics_to_python + # When self.value of is None, it might mean either it is + # not set or it is legitamately the None value. + # If self.value is legitimately None, we'll + # catch further down. + if hasattr(self, "value") and self.value is not None: + return self.value + n_evaluation = kwargs.get("n_evaluation", None) assert n_evaluation is None diff --git a/mathics/core/list.py b/mathics/core/list.py index b91f954dc..6f415e63e 100644 --- a/mathics/core/list.py +++ b/mathics/core/list.py @@ -25,7 +25,7 @@ class ListExpression(Expression): Keyword Arguments: - ``elements_properties`` -- properties of the collection of elements - - ``literal_values`` -- if this is not ``None``, then it is a tuple of Python values + - ``literal_values`` -- if this is not ``None``, then it is a tuple of Python values and the expression is a literal. """ _is_literal: bool @@ -49,16 +49,16 @@ def __init__( # call_frame = inspect.getouterframes(curframe, 2) # print("caller name:", call_frame[1][3]) - # from mathics.core.element import BaseElement - # for element in elements: - # if not isinstance(element, BaseElement): - # from trepan.api import debug; debug() - self._elements = elements - self.value = literal_values + + # When self.value is not None it a Python tuple (not Python + # list) sort that is the Python equivalent value for the Mathics3 list. # Check for literalness if it is not known - if literal_values is None: + if literal_values is not None: + self._is_literal = True + self.value = literal_values + else: self._is_literal = True values = [] for element in elements: diff --git a/mathics/core/streams.py b/mathics/core/streams.py index 2b98e92dc..d4f6a73fe 100644 --- a/mathics/core/streams.py +++ b/mathics/core/streams.py @@ -85,7 +85,7 @@ def path_search(filename: str) -> Tuple[Optional[str], bool]: result = urlsave_tmp(filename) is_temporary_file = True else: - for p in PATH_VAR + [""]: + for p in list(PATH_VAR) + [""]: path = canonic_filename(osp.join(p, filename)) if osp.exists(path): result = path diff --git a/mathics/eval/drawing/plot.py b/mathics/eval/drawing/plot.py index 9459744a1..c814f871a 100644 --- a/mathics/eval/drawing/plot.py +++ b/mathics/eval/drawing/plot.py @@ -131,7 +131,7 @@ def get_plot_range_option( "System`Automatic", "System`All", ) - or isinstance(pr, list) + or isinstance(pr, (list, tuple)) for pr in plotrange ) return plotrange diff --git a/mathics/eval/drawing/plot3d.py b/mathics/eval/drawing/plot3d.py index cf0e7a56c..341cddfff 100644 --- a/mathics/eval/drawing/plot3d.py +++ b/mathics/eval/drawing/plot3d.py @@ -49,441 +49,435 @@ def eval_plot3d( ): """%(name)s[functions_, {x_Symbol, xstart_, xstop_}, {y_Symbol, ystart_, ystop_}, OptionsPattern[%(name)s]]""" - if True: - xexpr_limits = ListExpression(x, xstart, xstop) - yexpr_limits = ListExpression(y, ystart, ystop) - expr = Expression( - Symbol(self.get_name()), - functions, - xexpr_limits, - yexpr_limits, - *options_to_rules(options), - ) - - functions = self.get_functions_param(functions) - plot_name = self.get_name() - - def convert_limit(value, limits): - result = value.round_to_float(evaluation) - if result is None: - evaluation.message(plot_name, "plln", value, limits) - return result - xstart = convert_limit(xstart, xexpr_limits) - xstop = convert_limit(xstop, xexpr_limits) - ystart = convert_limit(ystart, yexpr_limits) - ystop = convert_limit(ystop, yexpr_limits) - if None in (xstart, xstop, ystart, ystop): - return - - if ystart >= ystop: - evaluation.message(plot_name, "plln", ystop, expr) - return - - if xstart >= xstop: - evaluation.message(plot_name, "plln", xstop, expr) - return + xexpr_limits = ListExpression(x, xstart, xstop) + yexpr_limits = ListExpression(y, ystart, ystop) + expr = Expression( + Symbol(self.get_name()), + functions, + xexpr_limits, + yexpr_limits, + *options_to_rules(options), + ) - # Mesh Option - mesh_option = self.get_option(options, "Mesh", evaluation) - mesh = mesh_option.to_python() - if mesh not in ["System`None", "System`Full", "System`All"]: - evaluation.message("Mesh", "ilevels", mesh_option) - mesh = "System`Full" - - # PlotPoints Option - plotpoints_option = self.get_option(options, "PlotPoints", evaluation) - plotpoints = plotpoints_option.to_python() - - def check_plotpoints(steps): - if isinstance(steps, int) and steps > 0: - return True - return False - - if plotpoints == "System`None": - plotpoints = [7, 7] - elif check_plotpoints(plotpoints): - plotpoints = [plotpoints, plotpoints] - - if not ( - isinstance(plotpoints, list) - and len(plotpoints) == 2 - and check_plotpoints(plotpoints[0]) - and check_plotpoints(plotpoints[1]) - ): - evaluation.message(self.get_name(), "invpltpts", plotpoints) - plotpoints = [7, 7] - - # MaxRecursion Option - maxrec_option = self.get_option(options, "MaxRecursion", evaluation) - max_depth = maxrec_option.to_python() - if isinstance(max_depth, int): - if max_depth < 0: - max_depth = 0 - evaluation.message(self.get_name(), "invmaxrec", max_depth, 15) - elif max_depth > 15: - max_depth = 15 - evaluation.message(self.get_name(), "invmaxrec", max_depth, 15) - else: - pass # valid - elif max_depth == float("inf"): + functions = self.get_functions_param(functions) + plot_name = self.get_name() + + def convert_limit(value, limits): + result = value.round_to_float(evaluation) + if result is None: + evaluation.message(plot_name, "plln", value, limits) + return result + + xstart = convert_limit(xstart, xexpr_limits) + xstop = convert_limit(xstop, xexpr_limits) + ystart = convert_limit(ystart, yexpr_limits) + ystop = convert_limit(ystop, yexpr_limits) + if None in (xstart, xstop, ystart, ystop): + return + + if ystart >= ystop: + evaluation.message(plot_name, "plln", ystop, expr) + return + + if xstart >= xstop: + evaluation.message(plot_name, "plln", xstop, expr) + return + + # Mesh Option + mesh_option = self.get_option(options, "Mesh", evaluation) + mesh = mesh_option.to_python() + if mesh not in ["System`None", "System`Full", "System`All"]: + evaluation.message("Mesh", "ilevels", mesh_option) + mesh = "System`Full" + + # PlotPoints Option + plotpoints_option = self.get_option(options, "PlotPoints", evaluation) + plotpoints = plotpoints_option.to_python() + + def check_plotpoints(steps): + if isinstance(steps, int) and steps > 0: + return True + return False + + if plotpoints == "System`None": + plotpoints = (7, 7) + elif check_plotpoints(plotpoints): + plotpoints = (plotpoints, plotpoints) + + if not ( + isinstance(plotpoints, (list, tuple)) + and len(plotpoints) == 2 + and check_plotpoints(plotpoints[0]) + and check_plotpoints(plotpoints[1]) + ): + evaluation.message(self.get_name(), "invpltpts", plotpoints) + plotpoints = (7, 7) + + # MaxRecursion Option + maxrec_option = self.get_option(options, "MaxRecursion", evaluation) + max_depth = maxrec_option.to_python() + if isinstance(max_depth, int): + if max_depth < 0: + max_depth = 0 + evaluation.message(self.get_name(), "invmaxrec", max_depth, 15) + elif max_depth > 15: max_depth = 15 evaluation.message(self.get_name(), "invmaxrec", max_depth, 15) else: - max_depth = 0 - evaluation.message(self.get_name(), "invmaxrec", max_depth, 15) + pass # valid + elif max_depth == float("inf"): + max_depth = 15 + evaluation.message(self.get_name(), "invmaxrec", max_depth, 15) + else: + max_depth = 0 + evaluation.message(self.get_name(), "invmaxrec", max_depth, 15) - # Plot the functions - graphics = [] - for indx, f in enumerate(functions): - stored = {} + # Plot the functions + graphics = [] + for _, f in enumerate(functions): + stored = {} - compiled_fn = compile_quiet_function( - f, [x.get_name(), y.get_name()], evaluation, False - ) + compiled_fn = compile_quiet_function( + f, [x.get_name(), y.get_name()], evaluation, False + ) - def apply_fn(compiled_fn: Callable, x_value, y_value): - try: - # Try to used cached value first - return stored[(x_value, y_value)] - except KeyError: - value = compiled_fn(x_value, y_value) - if value is not None: - value = float(value) - stored[(x_value, y_value)] = value - return value - - triangles = [] - - split_edges = set() # subdivided edges - - def triangle(x1, y1, x2, y2, x3, y3, depth=0): - v1, v2, v3 = ( - apply_fn(compiled_fn, x1, y1), - apply_fn(compiled_fn, x2, y2), - apply_fn(compiled_fn, x3, y3), - ) + def apply_fn(compiled_fn: Callable, x_value, y_value): + try: + # Try to used cached value first + return stored[(x_value, y_value)] + except KeyError: + value = compiled_fn(x_value, y_value) + if value is not None: + value = float(value) + stored[(x_value, y_value)] = value + return value + + triangles = [] + + split_edges = set() # subdivided edges + + def triangle(x1, y1, x2, y2, x3, y3, depth=0): + v1, v2, v3 = ( + apply_fn(compiled_fn, x1, y1), + apply_fn(compiled_fn, x2, y2), + apply_fn(compiled_fn, x3, y3), + ) - if (v1 is v2 is v3 is None) and (depth > max_depth // 2): - # fast finish because the entire region is undefined but - # recurse 'a little' to avoid missing well defined regions - return - elif v1 is None or v2 is None or v3 is None: - # 'triforce' pattern recursion to find the edge of defined region - # 1 - # /\ - # 4 /__\ 6 - # /\ /\ - # /__\/__\ - # 2 5 3 - if depth < max_depth: - x4, y4 = 0.5 * (x1 + x2), 0.5 * (y1 + y2) - x5, y5 = 0.5 * (x2 + x3), 0.5 * (y2 + y3) - x6, y6 = 0.5 * (x1 + x3), 0.5 * (y1 + y3) - split_edges.add( - ((x1, y1), (x2, y2)) - if (x2, y2) > (x1, y1) - else ((x2, y2), (x1, y1)) - ) - split_edges.add( - ((x2, y2), (x3, y3)) - if (x3, y3) > (x2, y2) - else ((x3, y3), (x2, y2)) - ) - split_edges.add( - ((x1, y1), (x3, y3)) - if (x3, y3) > (x1, y1) - else ((x3, y3), (x1, y1)) - ) - triangle(x1, y1, x4, y4, x6, y6, depth + 1) - triangle(x4, y4, x2, y2, x5, y5, depth + 1) - triangle(x6, y6, x5, y5, x3, y3, depth + 1) - triangle(x4, y4, x5, y5, x6, y6, depth + 1) - return - triangles.append(sorted(((x1, y1, v1), (x2, y2, v2), (x3, y3, v3)))) - - # linear (grid) sampling - numx = plotpoints[0] * 1.0 - numy = plotpoints[1] * 1.0 - for xi in range(plotpoints[0]): - for yi in range(plotpoints[1]): - # Decide which way to break the square grid into triangles - # by looking at diagonal lengths. - # - # 3___4 3___4 - # |\ | | /| - # | \ | versus | / | - # |__\| |/__| - # 1 2 1 2 - # - # Approaching the boundary of the well defined region is - # important too. Use first strategy if 1 or 4 are undefined - # and strategy 2 if either 2 or 3 are undefined. - # - x1, x2, x3, x4 = ( - xstart + value * (xstop - xstart) - for value in ( - xi / numx, - (xi + 1) / numx, - xi / numx, - (xi + 1) / numx, - ) + if (v1 is v2 is v3 is None) and (depth > max_depth // 2): + # fast finish because the entire region is undefined but + # recurse 'a little' to avoid missing well defined regions + return + elif v1 is None or v2 is None or v3 is None: + # 'triforce' pattern recursion to find the edge of defined region + # 1 + # /\ + # 4 /__\ 6 + # /\ /\ + # /__\/__\ + # 2 5 3 + if depth < max_depth: + x4, y4 = 0.5 * (x1 + x2), 0.5 * (y1 + y2) + x5, y5 = 0.5 * (x2 + x3), 0.5 * (y2 + y3) + x6, y6 = 0.5 * (x1 + x3), 0.5 * (y1 + y3) + split_edges.add( + ((x1, y1), (x2, y2)) + if (x2, y2) > (x1, y1) + else ((x2, y2), (x1, y1)) ) - y1, y2, y3, y4 = ( - ystart + value * (ystop - ystart) - for value in ( - yi / numy, - yi / numy, - (yi + 1) / numy, - (yi + 1) / numy, - ) + split_edges.add( + ((x2, y2), (x3, y3)) + if (x3, y3) > (x2, y2) + else ((x3, y3), (x2, y2)) ) + split_edges.add( + ((x1, y1), (x3, y3)) + if (x3, y3) > (x1, y1) + else ((x3, y3), (x1, y1)) + ) + triangle(x1, y1, x4, y4, x6, y6, depth + 1) + triangle(x4, y4, x2, y2, x5, y5, depth + 1) + triangle(x6, y6, x5, y5, x3, y3, depth + 1) + triangle(x4, y4, x5, y5, x6, y6, depth + 1) + return + triangles.append(sorted(((x1, y1, v1), (x2, y2, v2), (x3, y3, v3)))) + + # linear (grid) sampling + numx = plotpoints[0] * 1.0 + numy = plotpoints[1] * 1.0 + for xi in range(plotpoints[0]): + for yi in range(plotpoints[1]): + # Decide which way to break the square grid into triangles + # by looking at diagonal lengths. + # + # 3___4 3___4 + # |\ | | /| + # | \ | versus | / | + # |__\| |/__| + # 1 2 1 2 + # + # Approaching the boundary of the well defined region is + # important too. Use first strategy if 1 or 4 are undefined + # and strategy 2 if either 2 or 3 are undefined. + # + x1, x2, x3, x4 = ( + xstart + value * (xstop - xstart) + for value in ( + xi / numx, + (xi + 1) / numx, + xi / numx, + (xi + 1) / numx, + ) + ) + y1, y2, y3, y4 = ( + ystart + value * (ystop - ystart) + for value in ( + yi / numy, + yi / numy, + (yi + 1) / numy, + (yi + 1) / numy, + ) + ) - v1 = apply_fn(compiled_fn, x1, y1) - v2 = apply_fn(compiled_fn, x2, y2) - v3 = apply_fn(compiled_fn, x3, y3) - v4 = apply_fn(compiled_fn, x4, y4) - - if v1 is None or v4 is None: - triangle(x1, y1, x2, y2, x3, y3) - triangle(x4, y4, x3, y3, x2, y2) - elif v2 is None or v3 is None: + v1 = apply_fn(compiled_fn, x1, y1) + v2 = apply_fn(compiled_fn, x2, y2) + v3 = apply_fn(compiled_fn, x3, y3) + v4 = apply_fn(compiled_fn, x4, y4) + + if v1 is None or v4 is None: + triangle(x1, y1, x2, y2, x3, y3) + triangle(x4, y4, x3, y3, x2, y2) + elif v2 is None or v3 is None: + triangle(x2, y2, x1, y1, x4, y4) + triangle(x3, y3, x4, y4, x1, y1) + else: + if abs(v3 - v2) > abs(v4 - v1): triangle(x2, y2, x1, y1, x4, y4) triangle(x3, y3, x4, y4, x1, y1) else: - if abs(v3 - v2) > abs(v4 - v1): - triangle(x2, y2, x1, y1, x4, y4) - triangle(x3, y3, x4, y4, x1, y1) - else: - triangle(x1, y1, x2, y2, x3, y3) - triangle(x4, y4, x3, y3, x2, y2) - - # adaptive resampling - # TODO: optimise this - # Cos of the maximum angle between successive line segments - ang_thresh = cos(20 * pi / 180) - for depth in range(1, max_depth): - needs_removal = set() - lent = len(triangles) # number of initial triangles - for i1 in range(lent): - for i2 in range(lent): - # find all edge pairings - if i1 == i2: - continue - t1 = triangles[i1] - t2 = triangles[i2] - - edge_pairing = ( - (t1[0], t1[1]) == (t2[0], t2[1]) - or (t1[0], t1[1]) == (t2[1], t2[2]) - or (t1[0], t1[1]) == (t2[0], t2[2]) - or (t1[1], t1[2]) == (t2[0], t2[1]) - or (t1[1], t1[2]) == (t2[1], t2[2]) - or (t1[1], t1[2]) == (t2[0], t2[2]) - or (t1[0], t1[2]) == (t2[0], t2[1]) - or (t1[0], t1[2]) == (t2[1], t2[2]) - or (t1[0], t1[2]) == (t2[0], t2[2]) - ) - if not edge_pairing: - continue - v1 = [t1[1][i] - t1[0][i] for i in range(3)] - w1 = [t1[2][i] - t1[0][i] for i in range(3)] - v2 = [t2[1][i] - t2[0][i] for i in range(3)] - w2 = [t2[2][i] - t2[0][i] for i in range(3)] - n1 = ( # surface normal for t1 - (v1[1] * w1[2]) - (v1[2] * w1[1]), - (v1[2] * w1[0]) - (v1[0] * w1[2]), - (v1[0] * w1[1]) - (v1[1] * w1[0]), - ) - n2 = ( # surface normal for t2 - (v2[1] * w2[2]) - (v2[2] * w2[1]), - (v2[2] * w2[0]) - (v2[0] * w2[2]), - (v2[0] * w2[1]) - (v2[1] * w2[0]), - ) - try: - angle = ( - n1[0] * n2[0] + n1[1] * n2[1] + n1[2] * n2[2] - ) / sqrt( - (n1[0] ** 2 + n1[1] ** 2 + n1[2] ** 2) - * (n2[0] ** 2 + n2[1] ** 2 + n2[2] ** 2) - ) - except ZeroDivisionError: - angle = 0.0 - if abs(angle) < ang_thresh: - for i, t in ((i1, t1), (i2, t2)): - # subdivide - x1, y1 = t[0][0], t[0][1] - x2, y2 = t[1][0], t[1][1] - x3, y3 = t[2][0], t[2][1] - x4, y4 = 0.5 * (x1 + x2), 0.5 * (y1 + y2) - x5, y5 = 0.5 * (x2 + x3), 0.5 * (y2 + y3) - x6, y6 = 0.5 * (x1 + x3), 0.5 * (y1 + y3) - needs_removal.add(i) - split_edges.add( - ((x1, y1), (x2, y2)) - if (x2, y2) > (x1, y1) - else ((x2, y2), (x1, y1)) - ) - split_edges.add( - ((x2, y2), (x3, y3)) - if (x3, y3) > (x2, y2) - else ((x3, y3), (x2, y2)) - ) - split_edges.add( - ((x1, y1), (x3, y3)) - if (x3, y3) > (x1, y1) - else ((x3, y3), (x1, y1)) - ) - triangle(x1, y1, x4, y4, x6, y6, depth=depth) - triangle(x2, y2, x4, y4, x5, y5, depth=depth) - triangle(x3, y3, x5, y5, x6, y6, depth=depth) - triangle(x4, y4, x5, y5, x6, y6, depth=depth) - # remove subdivided triangles which have been divided - triangles = [ - t for i, t in enumerate(triangles) if i not in needs_removal - ] - - # fix up subdivided edges - # - # look at every triangle and see if its edges need updating. - # depending on how many edges require subdivision we proceede with - # one of two subdivision strategies - # - # TODO possible optimisation: don't look at every triangle again - made_changes = True - while made_changes: - made_changes = False - new_triangles = [] - for i, t in enumerate(triangles): - new_points = [] - if ((t[0][0], t[0][1]), (t[1][0], t[1][1])) in split_edges: - new_points.append([0, 1]) - if ((t[1][0], t[1][1]), (t[2][0], t[2][1])) in split_edges: - new_points.append([1, 2]) - if ((t[0][0], t[0][1]), (t[2][0], t[2][1])) in split_edges: - new_points.append([0, 2]) - - if len(new_points) == 0: + triangle(x1, y1, x2, y2, x3, y3) + triangle(x4, y4, x3, y3, x2, y2) + + # adaptive resampling + # TODO: optimise this + # Cos of the maximum angle between successive line segments + ang_thresh = cos(20 * pi / 180) + for depth in range(1, max_depth): + needs_removal = set() + lent = len(triangles) # number of initial triangles + for i1 in range(lent): + for i2 in range(lent): + # find all edge pairings + if i1 == i2: continue - made_changes = True - # 'triforce' subdivision - # 1 - # /\ - # 4 /__\ 6 - # /\ /\ - # /__\/__\ - # 2 5 3 - # if less than three edges require subdivision bisect them - # anyway but fake their values by averaging - x4 = 0.5 * (t[0][0] + t[1][0]) - y4 = 0.5 * (t[0][1] + t[1][1]) - v4 = stored.get((x4, y4), 0.5 * (t[0][2] + t[1][2])) - - x5 = 0.5 * (t[1][0] + t[2][0]) - y5 = 0.5 * (t[1][1] + t[2][1]) - v5 = stored.get((x5, y5), 0.5 * (t[1][2] + t[2][2])) - - x6 = 0.5 * (t[0][0] + t[2][0]) - y6 = 0.5 * (t[0][1] + t[2][1]) - v6 = stored.get((x6, y6), 0.5 * (t[0][2] + t[2][2])) - - if not (v4 is None or v6 is None): - new_triangles.append(sorted((t[0], (x4, y4, v4), (x6, y6, v6)))) - if not (v4 is None or v5 is None): - new_triangles.append(sorted((t[1], (x4, y4, v4), (x5, y5, v5)))) - if not (v5 is None or v6 is None): - new_triangles.append(sorted((t[2], (x5, y5, v5), (x6, y6, v6)))) - if not (v4 is None or v5 is None or v6 is None): - new_triangles.append( - sorted(((x4, y4, v4), (x5, y5, v5), (x6, y6, v6))) + t1 = triangles[i1] + t2 = triangles[i2] + + edge_pairing = ( + (t1[0], t1[1]) == (t2[0], t2[1]) + or (t1[0], t1[1]) == (t2[1], t2[2]) + or (t1[0], t1[1]) == (t2[0], t2[2]) + or (t1[1], t1[2]) == (t2[0], t2[1]) + or (t1[1], t1[2]) == (t2[1], t2[2]) + or (t1[1], t1[2]) == (t2[0], t2[2]) + or (t1[0], t1[2]) == (t2[0], t2[1]) + or (t1[0], t1[2]) == (t2[1], t2[2]) + or (t1[0], t1[2]) == (t2[0], t2[2]) + ) + if not edge_pairing: + continue + v1 = [t1[1][i] - t1[0][i] for i in range(3)] + w1 = [t1[2][i] - t1[0][i] for i in range(3)] + v2 = [t2[1][i] - t2[0][i] for i in range(3)] + w2 = [t2[2][i] - t2[0][i] for i in range(3)] + n1 = ( # surface normal for t1 + (v1[1] * w1[2]) - (v1[2] * w1[1]), + (v1[2] * w1[0]) - (v1[0] * w1[2]), + (v1[0] * w1[1]) - (v1[1] * w1[0]), + ) + n2 = ( # surface normal for t2 + (v2[1] * w2[2]) - (v2[2] * w2[1]), + (v2[2] * w2[0]) - (v2[0] * w2[2]), + (v2[0] * w2[1]) - (v2[1] * w2[0]), + ) + try: + angle = (n1[0] * n2[0] + n1[1] * n2[1] + n1[2] * n2[2]) / sqrt( + (n1[0] ** 2 + n1[1] ** 2 + n1[2] ** 2) + * (n2[0] ** 2 + n2[1] ** 2 + n2[2] ** 2) ) - triangles[i] = None - - triangles.extend(new_triangles) - triangles = [t for t in triangles if t is not None] - - # add the mesh - mesh_points = [] - if mesh == "System`Full": - for xi in range(plotpoints[0] + 1): - xval = xstart + xi / numx * (xstop - xstart) - mesh_row = [] - for yi in range(plotpoints[1] + 1): - yval = ystart + yi / numy * (ystop - ystart) - z = stored[(xval, yval)] - mesh_row.append((xval, yval, z)) - mesh_points.append(mesh_row) - - for yi in range(plotpoints[1] + 1): - yval = ystart + yi / numy * (ystop - ystart) - mesh_col = [] - for xi in range(plotpoints[0] + 1): - xval = xstart + xi / numx * (xstop - xstart) - z = stored[(xval, yval)] - mesh_col.append((xval, yval, z)) - mesh_points.append(mesh_col) - - # handle edge subdivisions - made_changes = True - while made_changes: - made_changes = False - for mesh_line in mesh_points: - i = 0 - while i < len(mesh_line) - 1: - x1, y1, v1 = mesh_line[i] - x2, y2, v2 = mesh_line[i + 1] - key = ( + except ZeroDivisionError: + angle = 0.0 + if abs(angle) < ang_thresh: + for i, t in ((i1, t1), (i2, t2)): + # subdivide + x1, y1 = t[0][0], t[0][1] + x2, y2 = t[1][0], t[1][1] + x3, y3 = t[2][0], t[2][1] + x4, y4 = 0.5 * (x1 + x2), 0.5 * (y1 + y2) + x5, y5 = 0.5 * (x2 + x3), 0.5 * (y2 + y3) + x6, y6 = 0.5 * (x1 + x3), 0.5 * (y1 + y3) + needs_removal.add(i) + split_edges.add( ((x1, y1), (x2, y2)) if (x2, y2) > (x1, y1) else ((x2, y2), (x1, y1)) ) - if key in split_edges: - x3 = 0.5 * (x1 + x2) - y3 = 0.5 * (y1 + y2) - v3 = stored[(x3, y3)] - mesh_line.insert(i + 1, (x3, y3, v3)) - made_changes = True - i += 1 - i += 1 - - # handle missing regions - old_meshpoints, mesh_points = mesh_points, [] - for mesh_line in old_meshpoints: - mesh_points.extend( - [ - sorted(g) - for k, g in itertools.groupby( - mesh_line, lambda x: x[2] is None + split_edges.add( + ((x2, y2), (x3, y3)) + if (x3, y3) > (x2, y2) + else ((x3, y3), (x2, y2)) + ) + split_edges.add( + ((x1, y1), (x3, y3)) + if (x3, y3) > (x1, y1) + else ((x3, y3), (x1, y1)) ) - ] + triangle(x1, y1, x4, y4, x6, y6, depth=depth) + triangle(x2, y2, x4, y4, x5, y5, depth=depth) + triangle(x3, y3, x5, y5, x6, y6, depth=depth) + triangle(x4, y4, x5, y5, x6, y6, depth=depth) + # remove subdivided triangles which have been divided + triangles = [t for i, t in enumerate(triangles) if i not in needs_removal] + + # fix up subdivided edges + # + # look at every triangle and see if its edges need updating. + # depending on how many edges require subdivision we proceede with + # one of two subdivision strategies + # + # TODO possible optimisation: don't look at every triangle again + made_changes = True + while made_changes: + made_changes = False + new_triangles = [] + for i, t in enumerate(triangles): + new_points = [] + if ((t[0][0], t[0][1]), (t[1][0], t[1][1])) in split_edges: + new_points.append([0, 1]) + if ((t[1][0], t[1][1]), (t[2][0], t[2][1])) in split_edges: + new_points.append([1, 2]) + if ((t[0][0], t[0][1]), (t[2][0], t[2][1])) in split_edges: + new_points.append([0, 2]) + + if len(new_points) == 0: + continue + made_changes = True + # 'triforce' subdivision + # 1 + # /\ + # 4 /__\ 6 + # /\ /\ + # /__\/__\ + # 2 5 3 + # if less than three edges require subdivision bisect them + # anyway but fake their values by averaging + x4 = 0.5 * (t[0][0] + t[1][0]) + y4 = 0.5 * (t[0][1] + t[1][1]) + v4 = stored.get((x4, y4), 0.5 * (t[0][2] + t[1][2])) + + x5 = 0.5 * (t[1][0] + t[2][0]) + y5 = 0.5 * (t[1][1] + t[2][1]) + v5 = stored.get((x5, y5), 0.5 * (t[1][2] + t[2][2])) + + x6 = 0.5 * (t[0][0] + t[2][0]) + y6 = 0.5 * (t[0][1] + t[2][1]) + v6 = stored.get((x6, y6), 0.5 * (t[0][2] + t[2][2])) + + if not (v4 is None or v6 is None): + new_triangles.append(sorted((t[0], (x4, y4, v4), (x6, y6, v6)))) + if not (v4 is None or v5 is None): + new_triangles.append(sorted((t[1], (x4, y4, v4), (x5, y5, v5)))) + if not (v5 is None or v6 is None): + new_triangles.append(sorted((t[2], (x5, y5, v5), (x6, y6, v6)))) + if not (v4 is None or v5 is None or v6 is None): + new_triangles.append( + sorted(((x4, y4, v4), (x5, y5, v5), (x6, y6, v6))) ) - mesh_points = [ - mesh_line - for mesh_line in mesh_points - if not any(x[2] is None for x in mesh_line) - ] - elif mesh == "System`All": - mesh_points = set() - for t in triangles: - mesh_points.add((t[0], t[1]) if t[1] > t[0] else (t[1], t[0])) - mesh_points.add((t[1], t[2]) if t[2] > t[1] else (t[2], t[1])) - mesh_points.add((t[0], t[2]) if t[2] > t[0] else (t[2], t[0])) - mesh_points = list(mesh_points) - - # find the max and min height - v_min = v_max = None - for t in triangles: - for tx, ty, v in t: - if v_min is None or v < v_min: - v_min = v - if v_max is None or v > v_max: - v_max = v - graphics.extend( - self.construct_graphics( - triangles, mesh_points, v_min, v_max, options, evaluation + triangles[i] = None + + triangles.extend(new_triangles) + triangles = [t for t in triangles if t is not None] + + # add the mesh + mesh_points = [] + if mesh == "System`Full": + for xi in range(plotpoints[0] + 1): + xval = xstart + xi / numx * (xstop - xstart) + mesh_row = [] + for yi in range(plotpoints[1] + 1): + yval = ystart + yi / numy * (ystop - ystart) + z = stored[(xval, yval)] + mesh_row.append((xval, yval, z)) + mesh_points.append(mesh_row) + + for yi in range(plotpoints[1] + 1): + yval = ystart + yi / numy * (ystop - ystart) + mesh_col = [] + for xi in range(plotpoints[0] + 1): + xval = xstart + xi / numx * (xstop - xstart) + z = stored[(xval, yval)] + mesh_col.append((xval, yval, z)) + mesh_points.append(mesh_col) + + # handle edge subdivisions + made_changes = True + while made_changes: + made_changes = False + for mesh_line in mesh_points: + i = 0 + while i < len(mesh_line) - 1: + x1, y1, v1 = mesh_line[i] + x2, y2, v2 = mesh_line[i + 1] + key = ( + ((x1, y1), (x2, y2)) + if (x2, y2) > (x1, y1) + else ((x2, y2), (x1, y1)) + ) + if key in split_edges: + x3 = 0.5 * (x1 + x2) + y3 = 0.5 * (y1 + y2) + v3 = stored[(x3, y3)] + mesh_line.insert(i + 1, (x3, y3, v3)) + made_changes = True + i += 1 + i += 1 + + # handle missing regions + old_meshpoints, mesh_points = mesh_points, [] + for mesh_line in old_meshpoints: + mesh_points.extend( + [ + sorted(g) + for k, g in itertools.groupby(mesh_line, lambda x: x[2] is None) + ] ) + mesh_points = [ + mesh_line + for mesh_line in mesh_points + if not any(x[2] is None for x in mesh_line) + ] + elif mesh == "System`All": + mesh_points = set() + for t in triangles: + mesh_points.add((t[0], t[1]) if t[1] > t[0] else (t[1], t[0])) + mesh_points.add((t[1], t[2]) if t[2] > t[1] else (t[2], t[1])) + mesh_points.add((t[0], t[2]) if t[2] > t[0] else (t[2], t[0])) + mesh_points = list(mesh_points) + + # find the max and min height + v_min = v_max = None + for t in triangles: + for tx, ty, v in t: + if v_min is None or v < v_min: + v_min = v + if v_max is None or v > v_max: + v_max = v + graphics.extend( + self.construct_graphics( + triangles, mesh_points, v_min, v_max, options, evaluation ) - return self.final_graphics(graphics, options) + ) + return self.final_graphics(graphics, options) def construct_density_plot( diff --git a/mathics/eval/files_io/read.py b/mathics/eval/files_io/read.py index 26d74482a..c76d16584 100644 --- a/mathics/eval/files_io/read.py +++ b/mathics/eval/files_io/read.py @@ -168,7 +168,7 @@ def parse_read_options(options) -> dict: record_separators = options["System`RecordSeparators"].to_python( string_quotes=False ) - assert isinstance(record_separators, list) + assert isinstance(record_separators, (list, tuple)) # assert all( # isinstance(s, str) and s[0] == s[-1] == '"' for s in record_separators # ) @@ -180,7 +180,7 @@ def parse_read_options(options) -> dict: word_separators = options["System`WordSeparators"].to_python( string_quotes=False ) - assert isinstance(word_separators, list) + assert isinstance(word_separators, (list, tuple)) result["WordSeparators"] = word_separators # NullRecords @@ -308,7 +308,7 @@ def read_check_options(options: dict, evaluation: Evaluation) -> Optional[dict]: record_separators = options["System`RecordSeparators"].to_python( string_quotes=False ) - assert isinstance(record_separators, list) + assert isinstance(record_separators, (list, tuple)) result["RecordSeparators"] = record_separators # WordSeparators @@ -316,7 +316,7 @@ def read_check_options(options: dict, evaluation: Evaluation) -> Optional[dict]: word_separators = options["System`WordSeparators"].to_python( string_quotes=False ) - assert isinstance(word_separators, list) + assert isinstance(word_separators, (list, tuple)) result["WordSeparators"] = word_separators # NullRecords @@ -334,7 +334,9 @@ def read_check_options(options: dict, evaluation: Evaluation) -> Optional[dict]: # TokenWords if "System`TokenWords" in keys: token_words = options["System`TokenWords"].to_python(string_quotes=False) - if not (isinstance(token_words, list) or isinstance(token_words, String)): + if not ( + isinstance(token_words, (list, tuple)) or isinstance(token_words, String) + ): evaluation.message("ReadList", "opstl", token_words) return None result["TokenWords"] = token_words @@ -344,7 +346,7 @@ def read_check_options(options: dict, evaluation: Evaluation) -> Optional[dict]: def read_get_separators( options, evaluation: Evaluation -) -> Optional[Tuple[dict, dict, dict]]: +) -> Optional[Tuple[list, list, list]]: """Get record and word separators from apply "options".""" # Options # TODO Implement extra options @@ -357,7 +359,7 @@ def read_get_separators( token_words = py_options.get("TokenWords", {}) word_separators = py_options["WordSeparators"] - return record_separators, token_words, word_separators + return list(record_separators), list(token_words), list(word_separators) def read_from_stream( diff --git a/test/builtin/files_io/test_files.py b/test/builtin/files_io/test_files.py index c1decd9b3..61b98df4d 100644 --- a/test/builtin/files_io/test_files.py +++ b/test/builtin/files_io/test_files.py @@ -111,7 +111,7 @@ def test_close(): ('Close["abc"]', ("abc is not open.",), "Close[abc]", ""), ( "exp = Sin[1]; FilePrint[exp]", - ("File specification Sin[1] is not a string of one or more characters.",), + ("The specified argument, Sin[1], should be a valid string.",), "FilePrint[Sin[1]]", "", ), @@ -123,7 +123,7 @@ def test_close(): ), ( 'FilePrint[""]', - ("File specification is not a string of one or more characters.",), + ("The file name cannot be an empty string.",), "FilePrint[]", "", ), @@ -344,7 +344,12 @@ def test_close(): "Null", "", ), - ('FileDate[tmpfilename, "Access"]', None, "{2002, 1, 1, 0, 0, 0.}", ""), + ( + 'FileDate[tmpfilename, "Access"]', + None, + "{2002, 1, 1, 0, 0, 0.}", + "", + ), ("DeleteFile[tmpfilename]", None, "Null", ""), ], ) @@ -525,18 +530,6 @@ def test_write_string(): # def test_Inputget_and_put(): # stream = Expression('Plus', Symbol('x'), Integer(2)) -# TODO: add these Unix-specific test. Be sure not to test -# sys.platform for not Windows and to test for applicability -# ## writing to dir -# S> x >> /var/ -# : Cannot open /var/. -# = x >> /var/ - -# ## writing to read only file -# S> x >> /proc/uptime -# : Cannot open /proc/uptime. -# = x >> /proc/uptime - # ## writing to full file # S> x >> /dev/full # : No space left on device. diff --git a/test/core/test_sympy_python_convert.py b/test/core/test_sympy_python_convert.py index d9bd2a241..cae80f38c 100644 --- a/test/core/test_sympy_python_convert.py +++ b/test/core/test_sympy_python_convert.py @@ -225,7 +225,7 @@ def testComplex(self): self.compare(Complex(Integer1, Integer0), 1) def testList(self): - self.compare(ListExpression(Integer1), [1]) + self.compare(ListExpression(Integer1), (1,)) if __name__ == "__main__": diff --git a/test/test_to_python.py b/test/test_to_python.py index 21cd12234..1eb3ae7da 100644 --- a/test/test_to_python.py +++ b/test/test_to_python.py @@ -11,7 +11,7 @@ def test_to_infinity(): ), ( "PythonForm[{1, 2, 3, 4}]", - '"[1, 2, 3, 4]"', + '"(1, 2, 3, 4)"', "Simple List of integers", ), (