Skip to content

Commit 39acb51

Browse files
authored
Use ".value" in expr.to_python() and more often (#1417)
Make use of the ".value" field in BaseElement objects, especially Expression.to_python(). Recall, the parser creates Python equivalents for certain literal values. In particular, a list of strings. For this, a Python tuple of `str` appears. And this kind of thing happens a lot. And note that in contrast to Mathics3 strings, a Python string can be duplicated. So by using `.value` of strings found in parsing, we are not only saving time in creation by not copying, but also memory. And we save time in `to_python()` access as well. In some cases, like datetime objects, we can just skip `to_python()` and use `.value` instead. This motivation for this came from a desire to do a similar thing for SymPy and NumPy expressions, but that will have to wait. In the parser, list element literals are `tuple` types, whereas `to_python ()` returns `lists`. In the places where a Python list for a literal was expected, we have a Python tuple for the literal. In these cases, the code has been adjusted to allow both tuple and list types. Where certain list-only operations like mutation were needed, tuples are converted to lists in a new variable. Methods in module `mathics.core.atoms` have been put in alphabetical order. A bug in `convert_expression_elements` in setting `.value` was fixed. I suspect it wasn't noticed before because we were not making use of `.value`. Some of the `FilePrint` error messages have been aligned better for WMA's (and this makes the errors clearer, too.)
1 parent 454610e commit 39acb51

21 files changed

Lines changed: 611 additions & 597 deletions

File tree

mathics/builtin/datentime.py

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import sys
1414
import time
1515
from datetime import datetime, timedelta
16-
from typing import Optional
16+
from typing import Optional, Union
1717

1818
import dateutil.parser
1919

@@ -97,25 +97,18 @@
9797

9898
EPOCH_START = datetime(1900, 1, 1)
9999

100-
if not hasattr(timedelta, "total_seconds"):
101-
102-
def total_seconds(td):
103-
return (
104-
float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6)
105-
/ 10**6
106-
)
107-
108-
else:
109-
total_seconds = timedelta.total_seconds
100+
total_seconds = timedelta.total_seconds
110101

111102
SymbolDateObject = Symbol("DateObject")
112103
SymbolDateString = Symbol("DateString")
113104
SymbolGregorian = Symbol("Gregorian")
114105

115106

116107
class _Date:
117-
def __init__(self, datelist=[], absolute=None, datestr=None):
118-
datelist += [1900, 1, 1, 0, 0, 0.0][len(datelist) :]
108+
def __init__(
109+
self, datelist_arg: Union[list, tuple] = [], absolute=None, datestr=None
110+
):
111+
datelist = list(datelist_arg) + [1900, 1, 1, 0, 0, 0.0][len(datelist_arg) :]
119112
self.date = datetime(
120113
datelist[0],
121114
datelist[1],
@@ -249,7 +242,7 @@ def to_datelist(self, epochtime, evaluation: Evaluation):
249242
]
250243
return datelist
251244

252-
if not isinstance(etime, list):
245+
if not isinstance(etime, (list, tuple)):
253246
evaluation.message(form_name, "arg", etime)
254247
return
255248

@@ -258,7 +251,7 @@ def to_datelist(self, epochtime, evaluation: Evaluation):
258251
for i, val in enumerate(etime)
259252
):
260253
default_date = [1900, 1, 1, 0, 0, 0.0]
261-
datelist = etime + default_date[len(etime) :]
254+
datelist = list(etime) + default_date[len(etime) :]
262255
prec_part, imprec_part = datelist[:2], datelist[2:]
263256

264257
try:
@@ -289,12 +282,16 @@ def to_datelist(self, epochtime, evaluation: Evaluation):
289282
if len(etime) == 2:
290283
if (
291284
isinstance(etime[0], str)
292-
and isinstance(etime[1], list) # noqa
285+
and isinstance(etime[1], (list, tuple)) # noqa
293286
and all(isinstance(s, str) for s in etime[1])
294287
):
295288
is_spec = [
296289
str(s).strip('"') in DATE_STRING_FORMATS.keys() for s in etime[1]
297290
]
291+
292+
if isinstance(etime, tuple):
293+
etime = list(etime)
294+
298295
etime[1] = [str(s).strip('"') for s in etime[1]]
299296

300297
if sum(is_spec) == len(is_spec):
@@ -389,7 +386,7 @@ def eval_spec(self, epochtime, evaluation: Evaluation) -> Optional[MachineReal]:
389386
if datelist is None:
390387
return
391388

392-
date = _Date(datelist=datelist)
389+
date = _Date(datelist_arg=datelist)
393390
tdelta = date.date - EPOCH_START
394391
if tdelta.microseconds == 0:
395392
return Integer(int(total_seconds(tdelta)))
@@ -487,8 +484,8 @@ def eval(
487484
# Process dates
488485
pydate1, pydate2 = date1.to_python(), date2.to_python()
489486

490-
if isinstance(pydate1, list): # Date List
491-
idate = _Date(datelist=pydate1)
487+
if isinstance(pydate1, (list, tuple)): # Date List
488+
idate = _Date(datelist_arg=pydate1)
492489
elif isinstance(pydate1, (float, int)): # Absolute Time
493490
idate = _Date(absolute=pydate1)
494491
elif isinstance(pydate1, str): # Date string
@@ -497,8 +494,8 @@ def eval(
497494
evaluation.message("DateDifference", "date", date1)
498495
return
499496

500-
if isinstance(pydate2, list): # Date List
501-
fdate = _Date(datelist=pydate2)
497+
if isinstance(pydate2, (list, tuple)): # Date List
498+
fdate = _Date(datelist_arg=pydate2)
502499
elif isinstance(pydate2, (int, float)): # Absolute Time
503500
fdate = _Date(absolute=pydate2)
504501
elif isinstance(pydate1, str): # Date string
@@ -517,7 +514,9 @@ def eval(
517514
pyunits = units.to_python()
518515
if isinstance(pyunits, str):
519516
pyunits = [str(pyunits.strip('"'))]
520-
elif isinstance(pyunits, list) and all(isinstance(p, str) for p in pyunits):
517+
elif isinstance(pyunits, (list, tuple)) and all(
518+
isinstance(p, str) for p in pyunits
519+
):
521520
pyunits = [p.strip('"') for p in pyunits]
522521

523522
if not all(p in TIME_INCREMENTS.keys() for p in pyunits):
@@ -762,9 +761,9 @@ def eval(
762761

763762
# Process date
764763
pydate = date.to_python()
765-
if isinstance(pydate, list):
764+
if isinstance(pydate, (list, tuple)):
766765
date_prec = len(pydate)
767-
idate = _Date(datelist=pydate)
766+
idate = _Date(datelist_arg=pydate)
768767
elif isinstance(pydate, float) or isinstance(pydate, int):
769768
date_prec = "absolute"
770769
idate = _Date(absolute=pydate)
@@ -779,13 +778,17 @@ def eval(
779778
pyoff = off.to_python()
780779
if isinstance(pyoff, float) or isinstance(pyoff, int):
781780
pyoff = [[pyoff, '"Day"']]
782-
elif isinstance(pyoff, list) and len(pyoff) == 2 and isinstance(pyoff[1], str):
781+
elif (
782+
isinstance(pyoff, (list, tuple))
783+
and len(pyoff) == 2
784+
and isinstance(pyoff[1], str)
785+
):
783786
pyoff = [pyoff]
784787

785788
# Strip " marks
786789
pyoff = [[x[0], x[1].strip('"')] for x in pyoff]
787790

788-
if isinstance(pyoff, list) and all( # noqa
791+
if isinstance(pyoff, (list, tuple)) and all( # noqa
789792
len(o) == 2
790793
and o[1] in TIME_INCREMENTS.keys()
791794
and isinstance(o[0], (float, int))
@@ -940,15 +943,16 @@ def eval(
940943
self, epochtime: BaseElement, form: BaseElement, evaluation: Evaluation
941944
) -> Optional[String]:
942945
"DateString[epochtime_, form_]"
946+
943947
datelist = self.to_datelist(epochtime, evaluation)
944948

945949
if datelist is None:
946950
return
947951

948-
date = _Date(datelist=datelist)
952+
date = _Date(datelist_arg=datelist)
949953

950954
pyform = form.to_python()
951-
if not isinstance(pyform, list):
955+
if not isinstance(pyform, (list, tuple)):
952956
pyform = [pyform]
953957

954958
pyform = [x.strip('"') for x in pyform]

mathics/builtin/directories/directory_names.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,19 @@ class FileNameJoin(Builtin):
177177
def eval(self, pathlist, evaluation: Evaluation, options: dict):
178178
"FileNameJoin[pathlist_List, OptionsPattern[FileNameJoin]]"
179179

180+
# Convert pathlist to a Python list, and strip leading and trailing
181+
# quotes if that appears.
180182
py_pathlist = pathlist.to_python()
181-
if not all(isinstance(p, str) and p[0] == p[-1] == '"' for p in py_pathlist):
183+
184+
if not all(isinstance(p, str) for p in py_pathlist):
182185
return
183-
py_pathlist = [p[1:-1] for p in py_pathlist]
184186

185-
operating_system = (
186-
options["System`OperatingSystem"].evaluate(evaluation).get_string_value()
187-
)
187+
if isinstance(py_pathlist, tuple):
188+
py_pathlist = list(py_pathlist)
189+
else:
190+
py_pathlist = [p[1:-1] if p[0] == p[-1] == '"' else p for p in py_pathlist]
191+
192+
operating_system = options["System`OperatingSystem"].evaluate(evaluation).value
188193

189194
if operating_system not in ["MacOSX", "Windows", "Unix"]:
190195
evaluation.message(

mathics/builtin/file_operations/file_properties.py

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,18 @@
44

55
import os
66
import os.path as osp
7-
import time
7+
from datetime import datetime
88

99
from mathics.builtin.exp_structure.size_and_sig import Hash
1010
from mathics.builtin.files_io.files import MathicsOpen
11-
from mathics.core.atoms import Real, String
11+
from mathics.core.atoms import String
1212
from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED
1313
from mathics.core.builtin import Builtin, MessageException
1414
from mathics.core.convert.expression import to_expression
15-
from mathics.core.convert.python import from_python
1615
from mathics.core.evaluation import Evaluation
17-
from mathics.core.expression import Expression
1816
from mathics.core.streams import path_search
1917
from mathics.core.symbols import Symbol, SymbolNull
20-
from mathics.core.systemsymbols import SymbolAbsoluteTime, SymbolFailed, SymbolNone
21-
from mathics.eval.nevaluator import eval_N
18+
from mathics.core.systemsymbols import SymbolFailed, SymbolNone
2219

2320
sort_order = "mathics.builtin.file-operations.file_properties"
2421

@@ -103,17 +100,17 @@ def eval(self, path, timetype, evaluation):
103100
evaluation.message("FileDate", "datetype")
104101
return
105102

106-
# Offset for system epoch
107-
epochtime_expr = Expression(
108-
SymbolAbsoluteTime, String(time.strftime("%Y-%m-%d %H:%M", time.gmtime(0)))
103+
dt_object = datetime.fromtimestamp(result)
104+
# Extract the year, month, day, hour, and minute into a tuple
105+
datetime_tuple = (
106+
dt_object.year,
107+
dt_object.month,
108+
dt_object.day,
109+
dt_object.hour,
110+
dt_object.minute,
111+
dt_object.second,
109112
)
110-
epochtime_N = eval_N(epochtime_expr, evaluation)
111-
if epochtime_N is None:
112-
return None
113-
epochtime = epochtime_N.to_python()
114-
result += epochtime
115-
116-
return to_expression("DateList", Real(result))
113+
return to_expression("DateList", datetime_tuple)
117114

118115
def eval_default(self, path, evaluation):
119116
"FileDate[path_]"
@@ -299,7 +296,7 @@ def eval(self, filename, datelist, attribute, evaluation):
299296

300297
# Check datelist
301298
if not (
302-
isinstance(py_datelist, list)
299+
isinstance(py_datelist, (list, tuple))
303300
and len(py_datelist) == 6
304301
and all(isinstance(d, int) for d in py_datelist[:-1])
305302
and isinstance(py_datelist[-1], float)
@@ -311,25 +308,13 @@ def eval(self, filename, datelist, attribute, evaluation):
311308
evaluation.message("SetFileDate", "datetype")
312309
return
313310

314-
epochtime = (
315-
to_expression(
316-
"AbsoluteTime", time.strftime("%Y-%m-%d %H:%M", time.gmtime(0))
317-
)
318-
.evaluate(evaluation)
319-
.to_python()
320-
)
321-
322-
stattime = to_expression("AbsoluteTime", from_python(py_datelist))
323-
stattime_N = eval_N(stattime, evaluation)
324-
if stattime_N is None:
325-
return
326-
327-
stattime = stattime_N.to_python() - epochtime
311+
file_timestamp = datetime(*py_datelist[:4]).timestamp()
328312

329313
try:
330314
os.stat(py_filename)
331315
if py_attr == '"Access"':
332-
os.utime(py_filename, (stattime, osp.getatime(py_filename)))
316+
os.utime(py_filename, (file_timestamp, osp.getatime(py_filename)))
317+
return SymbolNull
333318
if py_attr == '"Creation"':
334319
if os.name == "posix":
335320
evaluation.message("SetFileDate", "nocreationunix")
@@ -338,9 +323,9 @@ def eval(self, filename, datelist, attribute, evaluation):
338323
# TODO: Note: This is windows only
339324
return SymbolFailed
340325
if py_attr == '"Modification"':
341-
os.utime(py_filename, (osp.getatime(py_filename), stattime))
342-
if py_attr == "All":
343-
os.utime(py_filename, (stattime, stattime))
326+
os.utime(py_filename, (osp.getatime(py_filename), file_timestamp))
327+
elif py_attr == "All":
328+
os.utime(py_filename, (file_timestamp, file_timestamp))
344329
except OSError:
345330
# evaluation.message(...)
346331
return SymbolFailed

mathics/builtin/files_io/files.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,8 @@ class FilePrint(Builtin):
267267
"""
268268

269269
messages = {
270-
"fstr": (
271-
"File specification `1` is not a string of " "one or more characters."
272-
),
270+
"zstr": ("The file name cannot be an empty string."),
271+
"badfile": ("The specified argument, `1`, should be a valid string."),
273272
}
274273

275274
options = {
@@ -281,33 +280,35 @@ class FilePrint(Builtin):
281280

282281
def eval(self, path, evaluation: Evaluation, options: dict):
283282
"FilePrint[path_, OptionsPattern[FilePrint]]"
283+
284+
if not isinstance(path, String):
285+
evaluation.message("FilePrint", "badfile", path)
286+
return
287+
284288
pypath = path.to_python()
289+
285290
if not (
286291
isinstance(pypath, str)
287292
and pypath[0] == pypath[-1] == '"'
288293
and len(pypath) > 2
289294
):
290-
evaluation.message("FilePrint", "fstr", path)
295+
evaluation.message("FilePrint", "zstr", path)
291296
return
292-
pypath, _ = path_search(pypath[1:-1])
297+
resolved_pypath, _ = path_search(pypath[1:-1])
293298

294299
# Options
295300
record_separators = options["System`RecordSeparators"].to_python()
296-
assert isinstance(record_separators, list)
297-
assert all(
298-
isinstance(s, str) and s[0] == s[-1] == '"' for s in record_separators
299-
)
300-
record_separators = [s[1:-1] for s in record_separators]
301+
assert isinstance(record_separators, tuple)
301302

302-
if pypath is None:
303+
if resolved_pypath is None:
303304
evaluation.message("General", "noopen", path)
304305
return
305306

306-
if not osp.isfile(pypath):
307+
if not osp.isfile(resolved_pypath):
307308
return SymbolFailed
308309

309310
try:
310-
with MathicsOpen(pypath, "r") as f:
311+
with MathicsOpen(resolved_pypath, "r") as f:
311312
result = f.read()
312313
except IOError:
313314
evaluation.message("General", "noopen", path)
@@ -1374,18 +1375,26 @@ def eval(self, name, n, text, evaluation: Evaluation, options: dict):
13741375

13751376
stream = to_expression("InputStream", name, n)
13761377

1377-
if not isinstance(py_text, list):
1378+
if not isinstance(py_text, (list, tuple)):
13781379
py_text = [py_text]
13791380

1380-
if not all(isinstance(t, str) and t[0] == t[-1] == '"' for t in py_text):
1381+
if not all(isinstance(t, str) for t in py_text):
13811382
evaluation.message("Find", "unknown", to_expression("Find", stream, text))
13821383
return
13831384

1384-
py_text = [t[1:-1] for t in py_text]
1385+
# If py_text comes from a (literal) value, then there are no
1386+
# leading/trailing quotes around strings. If it is still
1387+
# possible that py_text can be a list, then there could be
1388+
# leading/traling quotes.
1389+
if isinstance(py_text, list):
1390+
py_text = [t[1:-1] if t[0] == t[-1] == '"' else t for t in py_text]
13851391

13861392
while True:
13871393
tmp = super(Find, self).eval(stream, Symbol("Record"), evaluation, options)
1388-
py_tmp = tmp.to_python()[1:-1]
1394+
if not isinstance(tmp, String):
1395+
return SymbolFailed
1396+
1397+
py_tmp = tmp.value
13891398

13901399
if py_tmp == "System`EndOfFile":
13911400
evaluation.message(

0 commit comments

Comments
 (0)