From d3fb5f3fcf913a9d8cdcd7f26a4cf4f8415a56b3 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 30 Apr 2026 20:39:37 -0400 Subject: [PATCH 1/8] Detect ZIP files better --- mathics/builtin/files_io/importexport.py | 50 +++++++++++++----- mathics/data/ExampleData/Einstein.txt | Bin 0 -> 53453 bytes .../data/ExampleData/PacletServer-Install.mx | Bin 0 -> 4532 bytes pyproject.toml | 1 + test/builtin/files_io/test_importexport.py | 12 +++++ 5 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 mathics/data/ExampleData/Einstein.txt create mode 100644 mathics/data/ExampleData/PacletServer-Install.mx diff --git a/mathics/builtin/files_io/importexport.py b/mathics/builtin/files_io/importexport.py index e7d8c5634..807804d77 100644 --- a/mathics/builtin/files_io/importexport.py +++ b/mathics/builtin/files_io/importexport.py @@ -25,6 +25,8 @@ from itertools import chain from urllib.error import HTTPError, URLError +import magic as python_magic + from mathics.builtin.pymimesniffer import magic from mathics.core.atoms import ByteArray from mathics.core.attributes import A_NO_ATTRIBUTES, A_PROTECTED, A_READ_PROTECTED @@ -106,6 +108,7 @@ "application/x-tex": "TeX", # Also TeX "application/xhtml+xml": "XHTML", "application/xml": "XML", + "application/zip": "ZIP", "audio/aiff": "AIFF", "audio/basic": "AU", # Also SND "audio/midi": "MIDI", @@ -2080,20 +2083,39 @@ def eval(self, filename: String, evaluation: Evaluation): return findfile path = findfile.value - if not FileFormat.detector: - loader = magic.MagicLoader() - loader.load() - FileFormat.detector = magic.MagicDetector(loader.mimetypes) - - mime = set(FileFormat.detector.match(path)) - - # If match fails match on extension only - if mime == set(): - mime, encoding = mimetypes.guess_type(path) - if mime is None: - mime = set() - else: - mime = set([mime]) + + # FileFormat classifies by getting a mime type `path`, even + # though the path doesn't have to be something received or + # transmitted over HTTP. + + if os.path.exists(path): + try: + # Use python_magic to determine the file type. + # This is the most accurate method since it looks inside the file + # for magic numbers. Therefore, if a JPEG file has been renamed with the + # file extension .txt, this will still figure out what's up. + mimetype = python_magic.from_file(path, mime=True) + if mimetype in mimetype_dict: + return String(mimetype_dict[mimetype]) + + except Exception: + pass + else: + if not FileFormat.detector: + loader = magic.MagicLoader() + loader.load() + FileFormat.detector = magic.MagicDetector(loader.mimetypes) + + mime = set(FileFormat.detector.match(path)) + + # If match fails match on extension only + if mime == set(): + mime, _ = mimetypes.guess_type(path) + if mime is None: + mime = set() + else: + mime = set([mime]) + result = [] for key in mimetype_dict.keys(): if key in mime: diff --git a/mathics/data/ExampleData/Einstein.txt b/mathics/data/ExampleData/Einstein.txt new file mode 100644 index 0000000000000000000000000000000000000000..3da318776ab5fda1b50adb60f27e0af32c5b88ac GIT binary patch literal 53453 zcmbTd2UJr}*YJBnAPK!Ay-0^YNUzc%^d3N(f+0XCp@t%Y^d`Oc8hTNYCW6ugL3*zO zA_x%?m7<{V;rqPnd7pdN`o4SDH(6`W@9f!gX3m~H=ggUzTrFK~0#tf%T{r*&fdHNB zA8>UEn8MK>C{8~wtS=I!!0GDm@2zm_7ShYp(+lG#8i;mBd!n4sj-p<^F1P-k-TJGp zpoPZx`J>R7d*bpE;`hA0eEoeL(f$&mZr(0eAIR?LYG~XuF*Ani8faaY1ps6^255{I z0TlpvV*Gv0wADH9Sle(yCjbI~7=Qrc0N{x9^ENbxBY^9mtEIu|cdhi7|2te?<8%SI zjsaj+M8|}a^S|Z)e-G41Z(n}^0GV88kaR-%A+I_5nmq#jz5mLOu9@z>`(Gv?{L8-A z6})D~zwGia{^!4R{>7?)*~t^*bglEZ&fZR*PJemun&ShosA~orTytyy8WniWbJxt{ zf%QaR^UrIh^F%rN0RSP{U%5XD>3Yp#*G%S%Fw?l^+W-I~bNLUx{~zp+3c9W*0BCr5 z2m7L3T>Uu(kb<1z^73+=I;cPol)t}-i6he8(btJn!^_j#5fcmm|GMYDxd58Kw&lDw zvW&RAjEsnc=(YL(9sZAr|JM3H!{4_3m&Om%|7tT3!`T1G{wMGM$h;~5K_X|CphiQNAe5 zby1erUWUfFTsJqy3GI*e!f>K7|GO6czs&X@b@+?_^6P7$#a#k)_e24zNjd=hWegys zB?Z8CCD$pSf47?fu_f@g=UH)l`j_8d^L6@v#s80pAoco`zz^-h`B$uAhTueEeFOgT zb)Wbf5dkCsB|rx-0qg)bAOMH}5`ZkA2&e*DfF58BAOLINE`S7F0Z+ge2m~Gi5kM^P z7)S%MfdZfuzyWnYGtdHb0Np?zFa*27 z1qp*BKyn~ukQT@QWCpSZIe=V17!Vfp5EKP^49Wl%fXYF2pl6^LpgzzDXd1K(+5+u^ zzJtyQzyxFjbOdY!d<3@$~jJ_HX4q6tz6@(3yj8VTA7`Uu_;%oA)9 zd?q*n17K1x1DFde0+s`7fQ`X6U}vx|I1HQ!&H-0|o55Y+Vel+?6MO*vMF=6JCFCR& zAyg#PA+#Vw68aE^5hf895Y`g56Alv25^fP55nd9J6R{Er6Dbhs65SzkAqpglCCVYH zCTb&kLo`pcLv#v(Ko}tW5Lt*0#2VrT34tU*iXl%Ry^v|hHsk~fg)%{fp-NC=s3X)L z8Vk*bHbP%RXP`UKpD<2kHIUl(yxed8Lc?x+Q`2hKQ@}Cs66rvQm6iyV6 zC<-asDP|}RDTyh0Db*2&Gb=pNHG z(M`}D(bLdN(Oc37(HGM9&~Gw;8Tc4<89W$L8CneJDXpSt7*Bp3GdQLS? z56(Q!LCzyCRxTYbELS<#1lPq){+kvzBX2&rxz0_(EywN5oyFbHeZ<4TW61N6r-5gM zmzYjNP)^WYuvl

Kr~XcL-g<# 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 J?cIO>*$UsA9bW(d literal 0 HcmV?d00001 diff --git a/mathics/data/ExampleData/PacletServer-Install.mx b/mathics/data/ExampleData/PacletServer-Install.mx new file mode 100644 index 0000000000000000000000000000000000000000..4c04ea3c6bdb6cabd51f2f6cb4a0a4ae66b8638e GIT binary patch literal 4532 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 S0096206G8w0AvvW0001xp1wQ) literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 0e60c7c5a..6d927ff4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "pillow >= 9.2", "pint >=0.24", # Earlier pint has problems with numpy 2.2.6 "python-dateutil", + "python-magic", # Pympler is used in ByteCount[] and MemoryInUse[]. "Pympler", "requests", diff --git a/test/builtin/files_io/test_importexport.py b/test/builtin/files_io/test_importexport.py index f4ce7a1e7..12884644e 100644 --- a/test/builtin/files_io/test_importexport.py +++ b/test/builtin/files_io/test_importexport.py @@ -263,6 +263,18 @@ def test_export(): ('FileFormat["ExampleData/Testosterone.svg"]', None, "SVG", None), ('FileFormat["ExampleData/colors.json"]', None, "JSON", None), ('FileFormat["ExampleData/InventionNo1.xml"]', None, "XML", None), + ( + 'FileFormat["ExampleData/PacletServer-Install.mx"]', + None, + "ZIP", + "Detect ZIP files", + ), + ( + 'FileFormat["ExampleData/Einstein.txt"]', + None, + "JPEG", + "JPEG stored as with .txt exension", + ), ], ) def test_importexport(str_expr, msgs, str_expected, fail_msg): From 39327d61deffc85a79ebdf43c5a076676ea9f8d9 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 30 Apr 2026 21:10:13 -0400 Subject: [PATCH 2/8] Add libmagic via brew for MacOS --- .github/workflows/macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 59fae41dd..875e9304d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -25,7 +25,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install OS dependencies run: | - brew install llvm@14 tesseract remake + brew install llvm@14 tesseract remake libmagic - name: Install Mathics3 dependencies run: | # We can comment out after next Mathics3-Scanner release From fdb59180b119af7b1547e22bd9067c37fe4083fd Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 1 May 2026 05:55:48 -0400 Subject: [PATCH 3/8] Windows CI install libmagic --- .github/workflows/windows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 820be57ec..b26ee7349 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -30,6 +30,7 @@ jobs: # so we will be safe here. Another possibility would be check and install # conditionally. choco install --force llvm + choco install libmagic # choco install tesseract set LLVM_DIR="C:\Program Files\LLVM" - name: Install Mathics3 with Python dependencies From df54960f66a2e01e92ace13bcb5984f553ff548e Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 1 May 2026 06:04:37 -0400 Subject: [PATCH 4/8] Why mimetype are used in file determination --- mathics/builtin/files_io/importexport.py | 26 ++++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/mathics/builtin/files_io/importexport.py b/mathics/builtin/files_io/importexport.py index 807804d77..6f7b7a86b 100644 --- a/mathics/builtin/files_io/importexport.py +++ b/mathics/builtin/files_io/importexport.py @@ -1636,21 +1636,23 @@ class ImportString(Import): rules = {} summary_text = "import elements from a string" - def eval(self, data, evaluation, options={}): - "ImportString[data_, OptionsPattern[]]" - return self.eval_elements(data, ListExpression(), evaluation, options) + def eval(self, filename, evaluation, options={}): + "ImportString[filename_, OptionsPattern[]]" + return self.eval_elements(filename, ListExpression(), evaluation, options) - def eval_element(self, data, element: String, evaluation, options={}): - "ImportString[data_, element_String, OptionsPattern[]]" + def eval_element(self, filename, element: String, evaluation, options={}): + "ImportString[filename_, element_String, OptionsPattern[]]" - return self.eval_elements(data, ListExpression(element), evaluation, options) + return self.eval_elements( + filename, ListExpression(element), evaluation, options + ) - def eval_elements(self, data, elements, evaluation, options={}): - "ImportString[data_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" - if not (isinstance(data, String)): - evaluation.message("ImportString", "string", data) + def eval_elements(self, filename, elements, evaluation, options={}): + "ImportString[filename_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" + if not (isinstance(filename, String)): + evaluation.message("ImportString", "string", filename) return SymbolFailed - path = data.value + path = filename.value def determine_filetype(): if not FileFormat.detector: @@ -2087,6 +2089,8 @@ def eval(self, filename: String, evaluation: Evaluation): # FileFormat classifies by getting a mime type `path`, even # though the path doesn't have to be something received or # transmitted over HTTP. + # mime types are standardized and do not change, while file + # descriptions or WL's codes are not and can change. if os.path.exists(path): try: From 7a6c6f3e2ae910e89b507d6b7c4b10806d0a2bff Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 1 May 2026 06:09:50 -0400 Subject: [PATCH 5/8] CI: choco libmagic is prerelease --- .github/workflows/windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b26ee7349..336cfe1e0 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -30,7 +30,7 @@ jobs: # so we will be safe here. Another possibility would be check and install # conditionally. choco install --force llvm - choco install libmagic + choco install --pre libmagic # choco install tesseract set LLVM_DIR="C:\Program Files\LLVM" - name: Install Mathics3 with Python dependencies From b9d5830dc4aa29e238c65aafe0a17fb3f0667338 Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 1 May 2026 11:15:43 -0400 Subject: [PATCH 6/8] WIP - major refactor; not working --- .github/workflows/windows.yml | 1 - mathics/builtin/files_io/importexport.py | 561 +-------- mathics/builtin/pymimesniffer/__init__.py | 2 - mathics/builtin/pymimesniffer/magic.py | 249 ---- mathics/builtin/pymimesniffer/mimetypes.xml | 1182 ------------------- mathics/core/systemsymbols.py | 5 + mathics/eval/files_io/importexport.py | 514 ++++++++ 7 files changed, 570 insertions(+), 1944 deletions(-) delete mode 100644 mathics/builtin/pymimesniffer/__init__.py delete mode 100644 mathics/builtin/pymimesniffer/magic.py delete mode 100644 mathics/builtin/pymimesniffer/mimetypes.xml create mode 100644 mathics/eval/files_io/importexport.py diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 336cfe1e0..820be57ec 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -30,7 +30,6 @@ jobs: # so we will be safe here. Another possibility would be check and install # conditionally. choco install --force llvm - choco install --pre libmagic # choco install tesseract set LLVM_DIR="C:\Program Files\LLVM" - name: Install Mathics3 with Python dependencies diff --git a/mathics/builtin/files_io/importexport.py b/mathics/builtin/files_io/importexport.py index 6f7b7a86b..33fc89070 100644 --- a/mathics/builtin/files_io/importexport.py +++ b/mathics/builtin/files_io/importexport.py @@ -3,7 +3,7 @@ r""" Importing and Exporting -Many kinds data formats can be read into \\Mathics. Variable +Many kinds data formats can be read into \Mathics. Variable :\$ExportFormats: /doc/reference-of-built-in-symbols/inputoutput-files-and-filesystem/importing-and-exporting/\$exportformats \ contains a list of file formats that are supported by @@ -25,39 +25,31 @@ from itertools import chain from urllib.error import HTTPError, URLError -import magic as python_magic - -from mathics.builtin.pymimesniffer import magic from mathics.core.atoms import ByteArray from mathics.core.attributes import A_NO_ATTRIBUTES, A_PROTECTED, A_READ_PROTECTED -from mathics.core.builtin import Builtin, Integer, Predefined, String, get_option +from mathics.core.builtin import Builtin, Integer, Predefined, String from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.streams import stream_manager -from mathics.core.symbols import Symbol, SymbolNull, SymbolTrue, strip_context -from mathics.core.systemsymbols import ( - SymbolByteArray, - SymbolFailed, - SymbolRule, - SymbolToString, +from mathics.core.symbols import Symbol, SymbolNull, SymbolTrue +from mathics.core.systemsymbols import SymbolByteArray, SymbolFailed, SymbolToString +from mathics.eval.files_io.files import eval_Close +from mathics.eval.files_io.importexport import ( + IMPORTERS, + MIMETYPE_TO_SHORTNAME, + eval_Import, + filetype_from_MIME_content, + filetype_from_path, + importer_exporter_options, ) -from mathics.eval.files_io.files import eval_Close, eval_Open # This tells documentation how to sort this module # Here we are also hiding "file_io" since this can erroneously appear at the top level. sort_order = "mathics.builtin.importing-and-exporting" -mimetypes.add_type("application/vnd.wolfram.mathematica.package", ".m") - -SymbolDeleteFile = Symbol("DeleteFile") -SymbolFileExtension = Symbol("FileExtension") -SymbolFileFormat = Symbol("FileFormat") -SymbolFindFile = Symbol("FindFile") -SymbolOpenWrite = Symbol("OpenWrite") -SymbolOutputStream = Symbol("OutputStream") SymbolStringToStream = Symbol("StringToStream") SymbolWriteString = Symbol("WriteString") @@ -67,127 +59,6 @@ # TODO: Add more file formats -mimetype_dict = { - "application/dbase": "DBF", - "application/dbf": "DBF", - "application/dicom": "DICOM", - "application/eps": "EPS", - "application/fits": "FITS", - "application/json": "JSON", - "application/mathematica": "NB", - "application/mbox": "MBOX", - "application/mdb": "MDB", - "application/msaccess": "MDB", - "application/octet-stream": "OBJ", - "application/pcx": "PCX", - "application/pdf": "PDF", - "application/postscript": "EPS", - "application/rss+xml": "RSS", - "application/rtf": "RTF", - "application/sla": "STL", - "application/tga": "TGA", - "application/vnd.google-earth.kml+xml": "KML", - "application/vnd.ms-excel": "XLS", - "application/vnd.ms-pki.stl": "STL", - "application/vnd.msaccess": "MDB", - "application/vnd.oasis.opendocument.spreadsheet": "ODS", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX", # nopep8 - "application/vnd.sun.xml.calc": "SXC", - "application/vnd.wolfram.cdf": "CDF", - "application/vnd.wolfram.cdf.text": "CDF", - "application/vnd.wolfram.mathematica.package": "Package", - "application/x-3ds": "3DS", - "application/x-cdf": "NASACDF", - "application/x-eps": "EPS", - "application/x-flac": "FLAC", - "application/x-font-bdf": "BDF", - "application/x-hdf": "HDF", - "application/x-msaccess": "MDB", - "application/x-netcdf": "NetCDF", - "application/x-shockwave-flash": "SWF", - "application/x-tex": "TeX", # Also TeX - "application/xhtml+xml": "XHTML", - "application/xml": "XML", - "application/zip": "ZIP", - "audio/aiff": "AIFF", - "audio/basic": "AU", # Also SND - "audio/midi": "MIDI", - "audio/x-aifc": "AIFF", - "audio/x-aiff": "AIFF", - "audio/x-flac": "FLAC", - "audio/x-wav": "WAV", - "chemical/seq-aa-fasta": "FASTA", - "chemical/seq-na-fasta": "FASTA", - "chemical/seq-na-fastq": "FASTQ", - "chemical/seq-na-genbank": "GenBank", - "chemical/seq-na-sff": "SFF", - "chemical/x-cif": "CIF", - "chemical/x-daylight-smiles": "SMILES", - "chemical/x-hin": "HIN", - "chemical/x-jcamp-dx": "JCAMP-DX", - "chemical/x-mdl-molfile": "MOL", - "chemical/x-mdl-sdf": "SDF", - "chemical/x-mdl-sdfile": "SDF", - "chemical/x-mdl-tgf": "TGF", - "chemical/x-mmcif": "CIF", - "chemical/x-mol2": "MOL2", - "chemical/x-mopac-input": "Table", - "chemical/x-pdb": "PDB", - "chemical/x-xyz": "XYZ", - "image/bmp": "BMP", - "image/eps": "EPS", - "image/fits": "FITS", - "image/gif": "GIF", - "image/jp2": "JPEG2000", - "image/jpeg": "JPEG", - "image/pbm": "PNM", - "image/pcx": "PCX", - "image/pict": "PICT", - "image/png": "PNG", - "image/svg+xml": "SVG", - "image/tga": "TGA", - "image/tiff": "TIFF", - "image/vnd.dxf": "DXF", - "image/vnd.microsoft.icon": "ICO", - "image/x-3ds": "3DS", - "image/x-dxf": "DXF", - "image/x-exr": "OpenEXR", - "image/x-icon": "ICO", - "image/x-ms-bmp": "BMP", - "image/x-pcx": "PCX", - "image/x-portable-anymap": "PNM", - "image/x-portable-bitmap": "PBM", - "image/x-portable-graymap": "PGM", - "image/x-portable-pixmap": "PPM", - "image/x-xbitmap": "XBM", - "model/vrml": "VRML", - "model/x-lwo": "LWO", - "model/x-pov": "POV", - "model/x3d+xml": "X3D", - "text/calendar": "ICS", - "text/comma-separated-values": "CSV", - "text/csv": "CSV", - "text/html": "HTML", - "text/mathml": "MathML", - "text/plain": "Text", - "text/rtf": "RTF", - "text/scriptlet": "SCT", - "text/tab-separated-values": "TSV", - "text/texmacs": "Text", - "text/vnd.graphviz": "DOT", - "text/x-comma-separated-values": "CSV", - "text/x-csrc": "C", - "text/x-tex": "TeX", - "text/x-vcalendar": "VCS", - "text/x-vcard": "VCF", - "text/xml": "XML", - "video/avi": "AVI", - "video/quicktime": "QuickTime", - "video/x-flv": "FLV", - # None: 'Binary', -} - -IMPORTERS = {} EXPORTERS = {} EXTENSIONMAPPINGS = { "*.3ds": "3DS", @@ -905,45 +776,6 @@ } -def _importer_exporter_options( - available_options, options, builtin_name: str, evaluation -): - stream_options = [] - custom_options = [] - remaining_options = options.copy() - - if available_options and available_options.has_form("List", None): - for name in available_options.elements: - if isinstance(name, String): - py_name = name.get_string_value() - elif isinstance(name, Symbol): - py_name = strip_context(name.get_name()) - else: - py_name = None - - if py_name: - option = get_option(remaining_options, py_name, evaluation, pop=True) - if option is not None: - expr = Expression(SymbolRule, String(py_name), option) - if py_name == "CharacterEncoding": - stream_options.append(expr) - else: - custom_options.append(expr) - - syntax_option = remaining_options.get("System`$OptionSyntax", None) - if syntax_option and syntax_option != Symbol("System`Ignore"): - # warn about unsupported options. - for name, value in remaining_options.items(): - evaluation.message( - builtin_name, - "optx", - Expression(SymbolRule, strip_context(name), value), - strip_context(builtin_name), - ) - - return stream_options, custom_options - - class ConverterDumpsExtensionMappings(Predefined): r""" ## :internal native symbol: @@ -1272,13 +1104,13 @@ def eval(self, url: String, elements, evaluation: Evaluation, options={}): f.close() # on some OS (e.g. Windows) all writers need to be closed before another - # reader (e.g. Import._import) can access it. so close the file here. + # reader (e.g. Import) can access it. so close the file here. os.close(temp_handle) def determine_filetype(): - return mimetype_dict.get(content_type) + return MIMETYPE_TO_SHORTNAME.get(content_type) - result = Import._import( + result = eval_Import( temp_path, determine_filetype, elements, evaluation, options ) except HTTPError as e: @@ -1316,16 +1148,16 @@ class Import(Builtin): :WMA link:https://reference.wolfram.com/language/ref/Import.html

-
'Import'["$file$"] -
imports data from a file. +
'Import'["$source$"] +
imports data from a $source$. -
'Import'["$file$", "$fmt$"] +
'Import'["$source$", "$fmt$"]
imports file assuming the specified file format. -
'Import'["$file$", $elements$] +
'Import'["$source$", $elements$]
imports the specified elements from a file. -
'Import'["$file$", {"$fmt$", $elements$}] +
'Import'["$source$", {"$fmt$", $elements$}]
imports the specified elements from a file assuming the specified file format.
'Import'["http://$url$", ...] and 'Import'["ftp://$url$", ...] @@ -1365,26 +1197,24 @@ class Import(Builtin): summary_text = "import elements from a file" - def eval(self, filename, evaluation, options={}): - "Import[filename_, OptionsPattern[]]" - return self.eval_elements(filename, ListExpression(), evaluation, options) + def eval(self, source, evaluation, options={}): + "Import[source_, OptionsPattern[]]" + return self.eval_elements(source, ListExpression(), evaluation, options) - def eval_element(self, filename, element: String, evaluation, options={}): + def eval_element(self, source, element: String, evaluation, options={}): "Import[filename_, element_String, OptionsPattern[]]" - return self.eval_elements( - filename, ListExpression(element), evaluation, options - ) + return self.eval_elements(source, ListExpression(element), evaluation, options) - def eval_elements(self, filename, elements, evaluation, options={}): + def eval_elements(self, source, elements, evaluation, options={}): "Import[filename_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" # Check filename - path = filename.to_python() + path = source.to_python() if not (isinstance(path, str) and path[0] == path[-1] == '"'): - evaluation.message("Import", "chtype", filename) + evaluation.message("Import", "chtype", source) return SymbolFailed # Load local file - findfile = Expression(SymbolFindFile, filename).evaluate(evaluation) + findfile = Expression(SymbolFindFile, source).evaluate(evaluation) if findfile is SymbolFailed: evaluation.message("Import", "nffil") @@ -1397,210 +1227,10 @@ def determine_filetype(): .get_string_value() ) - return self._import(findfile, determine_filetype, elements, evaluation, options) - - @staticmethod - def _import(findfile, determine_filetype, elements, evaluation, options, data=None): - current_predetermined_out = evaluation.predetermined_out - # Check elements - if elements.has_form("List", None): - elements = elements.get_elements() - else: - elements = [elements] - - for el in elements: - if not isinstance(el, String): - evaluation.message("Import", "noelem", el) - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - - elements = [el.get_string_value() for el in elements] - - # Determine file type - for el in elements: - if el in IMPORTERS.keys(): - filetype = el - elements.remove(el) - break - else: - filetype = determine_filetype() - - if filetype not in IMPORTERS.keys(): - evaluation.message("Import", "fmtnosup", filetype) - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - - # Load the importer - conditionals, default_function, posts, importer_options = IMPORTERS[filetype] - - stream_options, custom_options = _importer_exporter_options( - importer_options.get("System`Options"), options, "System`Import", evaluation - ) - - function_channels = importer_options.get("System`FunctionChannels") - - if function_channels is None: - # TODO message - if data is None: - evaluation.message("Import", "emptyfch") - else: - evaluation.message("ImportString", "emptyfch") - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - - default_element = importer_options.get("System`DefaultElement") - if default_element is None: - # TODO message - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - - def get_results(tmp_function, findfile): - if function_channels == ListExpression(String("FileNames")): - joined_options = list(chain(stream_options, custom_options)) - tmpfile = False - if findfile is None: - tmpfile = True - stream = Expression(SymbolOpenWrite).evaluate(evaluation) - findfile = stream.elements[0] - if data is not None: - Expression(SymbolWriteString, data).evaluate(evaluation) - else: - Expression(SymbolWriteString, String("")).evaluate(evaluation) - eval_Close(stream, evaluation) - stream = None - import_expression = Expression(tmp_function, findfile, *joined_options) - tmp = import_expression.evaluate(evaluation) - if tmp is SymbolFailed: - return SymbolFailed - if tmpfile: - Expression(SymbolDeleteFile, findfile).evaluate(evaluation) - elif function_channels == ListExpression(String("Streams")): - if findfile is None: - stream = Expression(SymbolStringToStream, data).evaluate(evaluation) - else: - mode = "r" - if options.get("System`BinaryFormat") is SymbolTrue: - if not mode.endswith("b"): - mode += "b" - - encoding_option = options.get("System`CharacterEncoding") - encoding = ( - encoding_option.value - if isinstance(encoding_option, String) - else None - ) - - stream = eval_Open( - name=findfile, - mode=mode, - stream_type="InputStream", - encoding=encoding, - evaluation=evaluation, - ) - if stream is None: - return - if stream.get_head_name() != "System`InputStream": - evaluation.message("Import", "nffil") - evaluation.predetermined_out = current_predetermined_out - return None - tmp = Expression(tmp_function, stream, *custom_options).evaluate( - evaluation - ) - eval_Close(stream, evaluation) - else: - # TODO message - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - tmp = tmp.get_elements() - if not all(expr.has_form("Rule", None) for expr in tmp): - evaluation.predetermined_out = current_predetermined_out - return None - - # return {a.get_string_value() : b for a,b in map(lambda x: - # x.get_elements(), tmp)} - evaluation.predetermined_out = current_predetermined_out - return {a.get_string_value(): b for a, b in (x.get_elements() for x in tmp)} + return eval_Import(findfile, determine_filetype, elements, evaluation, options) - # Perform the import - defaults = None - if not elements: - defaults = get_results(default_function, findfile) - if defaults is None: - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - elif defaults is SymbolFailed: - return SymbolFailed - if default_element is Symbol("Automatic"): - evaluation.predetermined_out = current_predetermined_out - return ListExpression( - *( - Expression(SymbolRule, String(key), defaults[key]) - for key in defaults.keys() - ) - ) - else: - result = defaults.get(default_element.get_string_value()) - if result is None: - evaluation.message( - "Import", "noelem", default_element, String(filetype) - ) - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - evaluation.predetermined_out = current_predetermined_out - return result - else: - assert len(elements) >= 1 - el = elements[0] - if el == "Elements": - defaults = get_results(default_function, findfile) - if defaults is None: - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - # Use set() to remove duplicates - evaluation.predetermined_out = current_predetermined_out - return from_python( - sorted( - set( - list(conditionals.keys()) - + list(defaults.keys()) - + list(posts.keys()) - ) - ) - ) - else: - if el in conditionals.keys(): - result = get_results(conditionals[el], findfile) - if result is None: - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - if len(list(result.keys())) == 1 and list(result.keys())[0] == el: - evaluation.predetermined_out = current_predetermined_out - return list(result.values())[0] - elif el in posts.keys(): - # TODO: allow use of conditionals - result = get_results(posts[el]) - if result is None: - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - else: - if defaults is None: - defaults = get_results(default_function, findfile) - if defaults is None: - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - if el in defaults.keys(): - evaluation.predetermined_out = current_predetermined_out - return defaults[el] - else: - evaluation.message( - "Import", "noelem", from_python(el), String(filetype) - ) - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - - -class ImportString(Import): +class ImportString(Builtin): """ :WMA link: @@ -1611,7 +1241,7 @@ class ImportString(Import):
imports data in the specified format from a string.
'ImportString'["$file$", $elements$] -
imports the specified elements from a string. +
imports the specified elements from a file $file$.
'ImportString'["$data$"]
attempts to determine the format of the string from its content. @@ -1636,56 +1266,28 @@ class ImportString(Import): rules = {} summary_text = "import elements from a string" - def eval(self, filename, evaluation, options={}): - "ImportString[filename_, OptionsPattern[]]" - return self.eval_elements(filename, ListExpression(), evaluation, options) + def eval(self, data, evaluation, options={}): + "ImportString[data_, OptionsPattern[]]" + return self.eval_elements(data, ListExpression(), evaluation, options) - def eval_element(self, filename, element: String, evaluation, options={}): - "ImportString[filename_, element_String, OptionsPattern[]]" + def eval_element(self, source: str, element: String, evaluation, options={}): + "ImportString[source_, element_String, OptionsPattern[]]" - return self.eval_elements( - filename, ListExpression(element), evaluation, options - ) + return self.eval_elements(source, ListExpression(element), evaluation, options) - def eval_elements(self, filename, elements, evaluation, options={}): - "ImportString[filename_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" - if not (isinstance(filename, String)): - evaluation.message("ImportString", "string", filename) + def eval_elements(self, source, elements, evaluation, options={}): + "ImportString[source_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" + if not (isinstance(source, String)): + evaluation.message("ImportString", "string", source) return SymbolFailed - path = filename.value - def determine_filetype(): - if not FileFormat.detector: - loader = magic.MagicLoader() - loader.load() - FileFormat.detector = magic.MagicDetector(loader.mimetypes) - mime = set(FileFormat.detector.match("", data=data.to_python())) - - result = [] - for key in mimetype_dict.keys(): - if key in mime: - result.append(mimetype_dict[key]) - - # The following fixes an extremely annoying behaviour on some (not all) - # installations of Windows, where we end up classifying .csv files als XLS. - if ( - len(result) == 1 - and result[0] == "XLS" - and path.lower().endswith(".csv") - ): - return String("CSV") - - if len(result) == 0: - result = "Binary" - elif len(result) == 1: - result = result[0] - else: - return None + py_source = source.value - return result + def determine_filetype(): + return filetype_from_MIME_content(py_source) - return self._import( - None, determine_filetype, elements, evaluation, options, data=data + return eval_Import( + None, determine_filetype, elements, evaluation, options, data=source ) @@ -1829,7 +1431,7 @@ def eval_elements(self, filename, expr, elems, evaluation, options={}): # Load the exporter exporter_symbol, exporter_options = EXPORTERS[format_spec[0]] function_channels = exporter_options.get("System`FunctionChannels") - stream_options, custom_options = _importer_exporter_options( + stream_options, custom_options = importer_exporter_options( exporter_options.get("System`Options"), options, "System`Export", evaluation ) @@ -1933,12 +1535,6 @@ def eval_elements(self, expr, elems, evaluation: Evaluation, **options): # Just to be sure that the following evaluations do not change the value of this property current_predetermined_out = evaluation.predetermined_out - # Infer format if not present - if format_spec is None: - # evaluation.message("ExportString", "infer", filename) - evaluation.predetermined_out = current_predetermined_out - return SymbolFailed - # First item in format_spec is the explicit format. # The other elements (if present) are compression formats @@ -1956,7 +1552,7 @@ def eval_elements(self, expr, elems, evaluation: Evaluation, **options): exporter_symbol, exporter_options = EXPORTERS[format_spec[0]] function_channels = exporter_options.get("System`FunctionChannels") - stream_options, custom_options = _importer_exporter_options( + stream_options, custom_options = importer_exporter_options( exporter_options.get("System`Options"), options, "System Options", @@ -1999,7 +1595,7 @@ def eval_elements(self, expr, elems, evaluation: Evaluation, **options): res = tmpstream.read() tmpstream.close() if sys.platform not in ("win32",): - # On Windows unlink make the second NamedTemporaryFIle + # On Windows unlink make the second NamedTemporaryFile # fail giving something like: # [WinError 32] The process cannot access the file because it is being used by another process: ... # \\AppData\\Local\\Temp\\Mathics3-ExportString35eo_rih.svg' @@ -2072,8 +1668,6 @@ class FileFormat(Builtin): summary_text = "determine the file format of a file" - detector = None - def eval(self, filename: String, evaluation: Evaluation): "FileFormat[filename_String]" @@ -2084,60 +1678,7 @@ def eval(self, filename: String, evaluation: Evaluation): ) return findfile - path = findfile.value - - # FileFormat classifies by getting a mime type `path`, even - # though the path doesn't have to be something received or - # transmitted over HTTP. - # mime types are standardized and do not change, while file - # descriptions or WL's codes are not and can change. - - if os.path.exists(path): - try: - # Use python_magic to determine the file type. - # This is the most accurate method since it looks inside the file - # for magic numbers. Therefore, if a JPEG file has been renamed with the - # file extension .txt, this will still figure out what's up. - mimetype = python_magic.from_file(path, mime=True) - if mimetype in mimetype_dict: - return String(mimetype_dict[mimetype]) - - except Exception: - pass - else: - if not FileFormat.detector: - loader = magic.MagicLoader() - loader.load() - FileFormat.detector = magic.MagicDetector(loader.mimetypes) - - mime = set(FileFormat.detector.match(path)) - - # If match fails match on extension only - if mime == set(): - mime, _ = mimetypes.guess_type(path) - if mime is None: - mime = set() - else: - mime = set([mime]) - - result = [] - for key in mimetype_dict.keys(): - if key in mime: - result.append(mimetype_dict[key]) - - # the following fixes an extremely annoying behaviour on some (not all) - # installations of Windows, where we end up classifying .csv files as XLS. - if len(result) == 1 and result[0] == "XLS" and path.lower().endswith(".csv"): - return String("CSV") - - if len(result) == 0: - result = "Binary" - elif len(result) == 1: - result = result[0] - else: - return None - - return from_python(result) + return filetype_from_path(findfile.value) class B64Decode(Builtin): diff --git a/mathics/builtin/pymimesniffer/__init__.py b/mathics/builtin/pymimesniffer/__init__.py deleted file mode 100644 index 56fafa58b..000000000 --- a/mathics/builtin/pymimesniffer/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- diff --git a/mathics/builtin/pymimesniffer/magic.py b/mathics/builtin/pymimesniffer/magic.py deleted file mode 100644 index 38d63b35f..000000000 --- a/mathics/builtin/pymimesniffer/magic.py +++ /dev/null @@ -1,249 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import logging -import os.path -import sys - - -class MagicRule: - def __init__( - self, - mimeType, - parentType, - extensions, - allowsLeadingWhiteSpace, - magicNumbers, - magicStrings, - ): - self.mimeType = mimeType - self.parentType = parentType - self.extensions = extensions - self.allowsLeadingWhiteSpace = allowsLeadingWhiteSpace - self.magicNumbers = magicNumbers - self.magicStrings = magicStrings - - def __repr__(self): - return "" % self.mimeType - - -class MagicDetector: - def __init__(self, mimetypes): - self.mimetypes = mimetypes - - def match(self, filename, data=None): - matches = {} - - if not data: - file = open(filename, "rb") - buf = b"" - elif isinstance(data, str): - from io import StringIO - - file = StringIO(data) - matches["text/plain"] = self.mimetypes["text/plain"] - buf = "" - elif hasattr(data, "read"): - buf = b"" - file = data - else: - from io import BytesIO - - file = BytesIO(data) - buf = b"" - - ext = os.path.splitext(filename)[1] - - if ext: - ext = ext[1:] - - for mimetype, rules in self.mimetypes.items(): - for rule in rules: - if rule.parentType and rule.parentType not in list(matches.keys()): - continue - - if rule.extensions and ext != "" and ext not in rule.extensions: - continue - - for offset, value in rule.magicNumbers: - if offset + len(value) > len(buf): - buf += file.read(offset + len(value) - len(buf)) - - if buf[offset : offset + len(value)] == value: - matches[mimetype] = rule - break - - for caseSensitive, value in rule.magicStrings: - if len(value) > len(buf): - buf += file.read(len(value) - len(buf)) - - if buf[: len(value)] == value: - matches[mimetype] = rule - break - - return list(matches.keys()) - - -class MagicLoader: - def __init__(self, filename=None): - if not filename: - filename = os.path.join(os.path.dirname(__file__), "mimetypes.xml") - - if not os.path.isfile(filename): - raise IOError("magic mime type database '%s' doesn't exists" % filename) - - self.filename = filename - self.mimetypes = {} - - def getText(self, node, name=None): - text = b"" - - if name: - for child in node.getElementsByTagName(name): - text += self.getText(child).encode("utf-8", "ignore") - else: - for child in node.childNodes: - if child.nodeType == child.TEXT_NODE: - text += child.data.encode("utf-8", "ignore") - - return text.decode("utf-8") - - def getAttr(self, node, attr, default=""): - if not node.hasAttribute(attr): - return default - - return type(default)(node.getAttribute(attr)) - - def load(self, filename=None): - from binascii import unhexlify - from xml.dom.minidom import parse - - dom = parse(filename or self.filename) - - logging.info("loading magic database from %s", filename or self.filename) - - descriptions = dom.getElementsByTagName("description") - - for desc in descriptions: - mimeType = self.getText(desc, "mimeType") - parentType = self.getText(desc, "parentType") - extensions = self.getText(desc, "extensions").split(",") - allowsLeadingWhiteSpace = ( - self.getText(desc, "allowsLeadingWhiteSpace") == "true" - ) - - magicNumbers = [] - - for magicNumber in desc.getElementsByTagName("magicNumber"): - encoding = self.getAttr(magicNumber, "encoding", "string") - offset = self.getAttr(magicNumber, "offset", 0) - value = self.getText(magicNumber) - - if encoding == "hex": - value = unhexlify(value.replace(" ", "").encode("ascii")) - - magicNumbers.append((offset, value)) - - magicStrings = [] - - for magicString in desc.getElementsByTagName("magicString"): - caseSensitive = not ( - self.getAttr(magicString, "caseSensitive") == "false" - ) - value = self.getText(magicString) - - magicStrings.append((caseSensitive, value)) - - self.mimetypes.setdefault(mimeType, []).append( - MagicRule( - mimeType, - parentType, - extensions, - allowsLeadingWhiteSpace, - magicNumbers, - magicStrings, - ) - ) - - logging.info( - "loaded %d rules for %d MIME types from magic database", - len(descriptions), - len(self.mimetypes), - ) - - return len(descriptions) - - def reload(self): - self.mimetypes = {} - self.load() - - -import unittest - - -class TestDetector(unittest.TestCase): - detector = None - - def setUp(self): - if not self.detector: - loader = MagicLoader() - loader.load() - self.detector = MagicDetector(loader.mimetypes) - - def testMagicNumber(self): - self.assertEqual(["application/zip"], self.detector.match("test.zip", "PKtest")) - self.assertEqual([], self.detector.match("test.zip", "_PKtest")) - self.assertEqual([], self.detector.match("test.zip1", "PKtest")) - - self.assertEqual( - ["application/gzip"], self.detector.match("test.gz", "\x1f\x8b\x08test") - ) - self.assertEqual( - ["application/gzip"], self.detector.match("test.tgz", "\x1f\x8b\x08test") - ) - self.assertEqual([], self.detector.match("test.gz1", "\x1f\x8b\x08test")) - self.assertEqual([], self.detector.match("test.gz", "\x1f \x8b\x08test")) - - padding = "".join([" " for _ in range(257)]) - - self.assertEqual( - ["application/x-tar"], - self.detector.match("test.tar", padding + "ustartest"), - ) - self.assertEqual([], self.detector.match("test.tar1", padding + "ustartest")) - self.assertEqual([], self.detector.match("test.tar", padding + "ust artest")) - - -class TestLoader(unittest.TestCase): - def testInit(self): - self.assertRaises(IOError, MagicLoader, "not_exists_file") - - self.assert_(MagicLoader().filename) - - def testLoad(self): - loader = MagicLoader() - - self.assertFalse(loader.mimetypes) - - self.assert_(loader.load() > 0) - - self.assert_(loader.mimetypes) - - -def dump(mimetypes): - for type, rules in mimetypes.items(): - print(type) - - for rule in rules: - print(("\textenions = %s" % rule.extensions)) - print(("\tmagic num = %s" % rule.magicNumbers)) - print(("\tmagic str = %s" % rule.magicStrings)) - - -if __name__ == "__main__": - logging.basicConfig( - level=logging.DEBUG if "-v" in sys.argv else logging.WARN, - format="%(asctime)s %(levelname)-8s %(message)s", - ) - - unittest.main() diff --git a/mathics/builtin/pymimesniffer/mimetypes.xml b/mathics/builtin/pymimesniffer/mimetypes.xml deleted file mode 100644 index acbd0fc6e..000000000 --- a/mathics/builtin/pymimesniffer/mimetypes.xml +++ /dev/null @@ -1,1182 +0,0 @@ - - - - - - - - - - application/zip - zip - PK - - - - application/gzip - gz,tgz - 1f 8b 08 - - - - application/x-compress - z - 1f 9d 90 - - - - application/bzip2 - bz2,tbz2 - 42 5a 68 39 31 - - - - application/x-tar - ustar - tar - - - - application/x-rar-compressed - rar - 52 61 72 21 1a - - - - application/stuffit - sit - SIT! - - - - application/binhex - hqx - - - - application/vnd.ms-cab-compressed - cab - MSCF - - - - application/x-installshield-compressedfile - ISc( - - - - - - text/html - html,htm,htc,shtml,jsp,jspf,php,asp,xhtml - true - <HTML - <HEAD - <BODY - <!DOCTYPE HTML - <!-- - <TITLE - <H1> - - - - text/xml - xml - <?xml - - - - - application/xhtml+xml - html,htm,htc,shtml,jsp,jspf,php,asp,xhtml - text/xml - - - - application/xslt+xml - xsl,xslt - text/xml - - - - text/vnd.wap.wml - wml - text/xml - - - - application/rdf+xml - rdf,rdfs - text/xml - - - - application/owl+xml - owl - text/xml - - - - application/trix - trix - text/xml - - - - application/x-turtle - ttl - - - - text/rdf+n3 - n3 - - - - text/css - css - - - - text/javascript - js - - - - application/json - json - - - - application/java-archive - jar - application/zip - - - - application/x-java-webarchive - war - application/zip - - - - application/x-java-enterprisearchive - ear - application/zip - - - - application/x-url - [InternetShortcut] - url - - - - application/vnd.adobe.air-application-installer-package+zip - air - application/zip - - - - - - application/vnd.sun.xml.calc - application/zip - sxc - - - - application/vnd.sun.xml.draw - application/zip - sxd - - - - application/vnd.sun.xml.impress - application/zip - sxi - - - - application/vnd.sun.xml.writer - application/zip - sxw - - - - application/vnd.sun.xml.math - application/zip - sxm - - - - application/vnd.sun.xml.calc.template - application/zip - stc - - - - application/vnd.sun.xml.draw.template - application/zip - std - - - - application/vnd.sun.xml.impress.template - application/zip - sti - - - - application/vnd.sun.xml.writer.template - application/zip - stw - - - - - - application/vnd.oasis.opendocument.spreadsheet - application/zip - ods - - - - application/vnd.oasis.opendocument.graphics - application/zip - odg - - - - application/vnd.oasis.opendocument.presentation - application/zip - odp - - - - application/vnd.oasis.opendocument.text - application/zip - odt - - - - application/vnd.oasis.opendocument.formula - application/zip - odf - - - - application/vnd.oasis.opendocument.spreadsheet-template - application/zip - ots - - - - application/vnd.oasis.opendocument.graphics-template - application/zip - otg - - - - application/vnd.oasis.opendocument.presentation-template - application/zip - otp - - - - application/vnd.oasis.opendocument.text-template - application/zip - ott - - - - - - - - application/vnd.ms-office - d0 cf 11 e0 a1 b1 1a e1 00 00 00 00 00 00 00 00 - - - - application/vnd.ms-word - application/vnd.ms-office - doc,dot - - - - application/vnd.ms-excel - application/vnd.ms-office - xls,xlt - - - - application/vnd.ms-powerpoint - application/vnd.ms-office - ppt,pot,pps - - - - application/vnd.visio - application/vnd.ms-office - vsd,vst,vss - - - - - - application/x-mspublisher - application/vnd.ms-office - pub - - - - application/x-slk - slk,sylk - - - - - - application/vnd.openxmlformats-officedocument.wordprocessingml - application/zip - docx,docm,dotx,dotm - - - - application/vnd.openxmlformats-officedocument.spreadsheetml - application/zip - xlsx,xlsm,xltx,xltm,xlsb,xlam - - - - application/vnd.openxmlformats-officedocument.presentationml - application/zip - pptx,pptm,potx,potm,ppam,ppsx,ppsm - - - - application/vnd.ms-xpsdocument - application/zip - xps - - - - - - - application/vnd.stardivision.impress - application/vnd.ms-office - sdd - - - - application/vnd.stardivision.draw - application/vnd.ms-office - sda - - - - application/vnd.stardivision.writer - application/vnd.ms-office - sdw - - - - application/vnd.stardivision.calc - application/vnd.ms-office - sdc - - - - - - - application/vnd.ms-works - application/vnd.ms-office - wps,xlr - - - - application/vnd.ms-works - wks - ff 00 02 00 04 04 05 54 02 00 - - - - application/vnd.ms-works-db - application/vnd.ms-office - wdb - - - - - - application/vnd.wordperfect - wp,wpd,wpf,wpt,wpw,wp5,wp51,wp6,w60,w61 - ff 57 50 43 - - - - application/x-quattropro - application/vnd.ms-office - qpw,wb3 - - - - application/wb2 - wb2 - 00 00 02 00 - - - - - application/presentations - application/vnd.ms-office - shw - - - - application/presentations - application/vnd.wordperfect - shw - - - - - - message/rfc822 - eml,mht,mhtml - - - Return-Path: - From: - Date: - Forward to - Pipe to - Relay-Version: - #! rnews - N#! rnews - - - Path: - Xref: - Article - - - - application/vnd.ms-outlook - pst - 21 42 44 4e - - - - application/vnd.ms-outlookexpress - dbx - 4a 4d 46 36 03 00 10 00 - cf ad 12 fe c5 fd 74 6f 66 e3 d1 11 9a 4e 00 c0 - - - - - - text/plain - txt,1st,me,text,ans,asc,csv,tsv,faq,c,h,tex,latex,pv,log,nt - - - - text/java - java - - - - application/x-java-manifest - Manifest-Version: - - - - text/rtf - rtf - {\rtf - - - - application/pdf - pdf - %PDF- - - - - application/x-framemaker - book,fm,mif,mf - <MakerFile - <MIFFile - <MakerDictionary - <MakerScreenFont - <MML - <BookFile - <Maker - - - - application/postscript - ps - %! - - - - application/winhlp - hlp - ?_ - - - - application/x-chm - chm - ITSF - - - - application/x-freemind - mm - <map version - - - - - - application/x-ms-dos-executable - exe - MZ - - - - application/x-ms-scr - application/x-ms-dos-executable - scr - - - - application/x-ms-shortcut - lnk - 4c 00 00 00 01 14 02 00 00 00 00 00 c0 00 00 00 00 00 00 46 - - - - application/bat - bat - - - - application/x-java-class - class - ca fe ba be - - - - application/x-sh - sh - #!/bin/sh - #!/usr/bin/sh - - - - application/x-csh - csh - #!/bin/csh - #!/usr/bin/csh - - - - application/x-bash - bash - #!/bin/bash - #!/usr/bin/bash - - - - application/x-ksh - ksh - #!/bin/ksh - #!/usr/bin/ksh - - - - application/x-tsh - tsh - #!/bin/tsh - #!/usr/bin/tsh - - - - application/x-applescript - scpt - - - - - - image/bmp - bmp - BM - - - - image/gif - gif - GIF8 - - - - image/jpeg - jpg,jpeg - ff d8 ff - - - - image/png - png - 89 50 4e 47 0d 0a 1a - - - - image/svg+xml - svg - text/xml - - - - image/x-icon - ico - 00 00 01 00 - - - - image/x-raw - raw - - - - image/x-tga - tga - - - - image/x-portable-bitmap - pbm - P1 - P4 - - - - image/x-portable-greymap - pgm - P2 - P5 - - - - image/x-portable-pixmap - ppm - P3 - P6 - - - - image/tiff - tif,tiff - 4d 4d 00 2a - 49 49 2a 00 - - - - image/dng - dng - image/tiff - - - - image/x-paintshoppro - psp - Paint Shop Pro Image File - - - - image/xcf - xcf - 67 69 6d 70 20 78 63 66 20 - - - - application/vnd.corel-draw - cdr - CDRA - - - - image/x-xfig - fig - #FIG - - - - image/wmf - wmf - d7 cd c6 9a 00 00 - 01 00 09 00 00 03 - - - - image/x-xbitmap - xbm - - - - image/xpm - xpm - 2f 2a 20 58 50 4d 20 2a 2f 0a - - - - image/x-dwf - dwf - (DWF - - - - image/x-dwg - dwg - AC - - - - image/x-dxf - dxf - - - - image/x-itunes-albumartwork - itc - itch - - - - - - video/x-ms-asf - asf - 30 26 b2 75 8e 66 cf 11 a6 d9 00 aa 00 62 ce 6c - - - - video/x-ms-asx - asx - <asx - <ASX - - - - audio/x-ms-wax - wax - - - - video/x-ms-wvx - wvx - - - - video/x-ms-wmx - wmx - - - - video/x-msvideo - avi - 41 56 49 20 - - - - - - application/x-ms-wm - 30 26 b2 75 8e 66 cf 11 a6 d9 00 aa 00 62 ce 6c - - - - audio/x-ms-wma - application/x-ms-wm - wma - - - - video/x-ms-wmv - application/x-ms-wm - wmv,wm - - - - video/quicktime - mov - moov - - - - video/mpeg - mpg,mpeg - 00 00 01 b3 - 00 00 01 ba - - - - application/x-shockwave-flash - swf - 46 57 53 - - - - application/x-ogg - ogg - OggS - - - - application/vnd.rn-realmedia - rm,ram - .RMF - rtsp:// - - - - audio/x-wav - wav - WAVE - - - - audio/mpeg - mp3,mp2 - ID3 - - - - audio/midi - mid,midi,rmi - MThd - RMI - - - - video/x-msvideo - avi - 41 56 49 20 - - - - video/mp4 - mp4,mpg4,m4v,mp4v,divx,xvid,264 - - - - audio/mp4 - m4a,m4p - - - - video/3gpp - 3gp,3g2 - - - - audio/x-aiff - aiff - FORM - - - - application/x-ms-wmd - wmd - application/zip - - - - video/x-flv - flv - FLV - - - - audio/flac - flac - 66 4c 61 43 00 00 00 22 - - - - application/smil - smi,smil - - - - - - application/x-winamp-playlist - m3u - #EXTM3U - - - - audio/x-b4s - text/xml - b4s - - - - application/xspf+xml - text/xml - xspf - - - - audio/x-scpls - pls - [playlist] - - - - audio/x-kpl - kpl - [Metadata] artist= - - - - audio/x-kapsule - text/xml - p2p - - - - audio/x-magma - magma - #MAGMA - - - - vnd.ms-wpl - wpl - <?wpl - - - - - - application/pgp-signature - -----BEGIN PGP SIGNATURE----- - - - - application/x-md5 - md5 - MD5 - - - - application/x-sha - sha,sha0,sha1,sha2,sha256,sha512 - - - - application/x-axcrypt - axx - c0 b9 07 2e 4f 93 f1 46 a0 15 79 2c a1 d9 e8 21 15 00 00 00 02 - - - - - - text/calendar - ics - BEGIN:VCALENDAR - - - - application/x-mozilla-addressbook - mab - - - - application/x-ms-registry - reg - regf - - - - application/x-bittorrent - torrent - d8:announce - - - - application/x-pom - pom - <project> - - - - application/x-ms-wmz - wmz - application/zip - - - - text/x-vcard - vcf,vcard - BEGIN:VCARD - - - diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 0b707705b..b6d2d016a 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -98,6 +98,7 @@ SymbolD = Symbol("System`D") SymbolDefault = Symbol("System`Default") SymbolDefinition = Symbol("System`Definition") +SymbolDeleteFile = Symbol("System`DeleteFile") SymbolDerivative = Symbol("System`Derivative") SymbolDigitCharacter = Symbol("System`DigitCharacter") SymbolDirectedInfinity = Symbol("System`DirectedInfinity") @@ -128,7 +129,10 @@ SymbolFaceGridsStyle = Symbol("System`FaceGridsStyle") SymbolFactorial = Symbol("System`Factorial") SymbolFailed = Symbol("System`$Failed") +SymbolFileExtension = Symbol("System`FileExtension") +SymbolFileFormat = Symbol("System`FileFormat") SymbolFindClusters = Symbol("System`FindClusters") +SymbolFindFile = Symbol("System`FindFile") SymbolFirst = Symbol("System`First") SymbolFloor = Symbol("System`Floor") SymbolFormBox = Symbol("System`FormBox") @@ -235,6 +239,7 @@ SymbolNumericQ = Symbol("System`NumericQ") SymbolO = Symbol("System`O") SymbolOpacity = Symbol("System`Opacity") +SymbolOpenWrite = Symbol("System`Openwrite") SymbolOperate = Symbol("System`Operate") SymbolOptionValue = Symbol("System`OptionValue") SymbolOptional = Symbol("System`Optional") diff --git a/mathics/eval/files_io/importexport.py b/mathics/eval/files_io/importexport.py new file mode 100644 index 000000000..ba5048903 --- /dev/null +++ b/mathics/eval/files_io/importexport.py @@ -0,0 +1,514 @@ +""" +Functions for figuring out a filetype or MIME type a given +file path. +""" + +import mimetypes +import os.path as osp +from typing import Dict, Final, Optional + +from mathics.core.builtin import String, get_option +from mathics.core.convert.python import from_python +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol, SymbolTrue, strip_context +from mathics.core.systemsymbols import SymbolFailed, SymbolRule +from mathics.eval.files_io.files import eval_Close, eval_Open + +IMPORTERS = {} + +try: + from magic import from_file +except ImportError: + + def from_file(path: str, mime: bool = False) -> str: + """ + Standard library implementation mimicking magic.from_file. + + Args: + path: Path to the file. + mime: If True, returns MIME type. If False, returns a description. + """ + # Guess the MIME type based on the file extension + # Example: 'image/jpeg' or 'text/x-python'. + mime_type, encoding = mimetypes.guess_type(path) + + # Handle cases where the extension is unknown. + if mime_type is None: + # Fallback to binary or plain text if the extension is missing. + mime_type = "application/octet-stream" + + if mime: + return mime_type + + # Mimic the 'description' behavior of libmagic Since mimetypes + # doesn't provide descriptions, we provide a clean label. + description = mime_type.split("/")[-1].replace("x-", "").upper() + + if encoding: + return f"{description} ({encoding} compressed)" + return f"{description} data" + + +# Note Matlab and Objective C also use the ".m" extension! +mimetypes.add_type("application/vnd.wolfram.mathematica.package", ".m") + +# Do we need the below? +# mimetypes.add_type("application/vnd.wolfram.mathematica.package", ".wl") + +# MIMETYPE_TO_SHORTNAME is a mapping form MIME type names to short common names. +# The short common names are typically used as a file extension. + +# Here we should have *only* the names used when the name differs +# from mimetypes.guess_extension(mimetype).upper() gives a name different +# from what we have here. This happens for lowercase or mixed-case names. + +# TODO: go over to remove names that do not need to be on this list. +MIMETYPE_TO_SHORTNAME: Final[Dict[str, str]] = { + "application/dbase": "DBF", + "application/dbf": "DBF", + "application/dicom": "DICOM", + "application/eps": "EPS", + "application/fits": "FITS", + "application/json": "JSON", + "application/mathematica": "NB", + "application/mbox": "MBOX", + "application/mdb": "MDB", + "application/msaccess": "MDB", + "application/octet-stream": "OBJ", + "application/pcx": "PCX", + "application/pdf": "PDF", + "application/postscript": "EPS", + "application/rss+xml": "RSS", + "application/rtf": "RTF", + "application/sla": "STL", + "application/tga": "TGA", + "application/vnd.google-earth.kml+xml": "KML", + "application/vnd.ms-excel": "XLS", + "application/vnd.ms-pki.stl": "STL", + "application/vnd.msaccess": "MDB", + "application/vnd.oasis.opendocument.spreadsheet": "ODS", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX", # nopep8 + "application/vnd.sun.xml.calc": "SXC", + "application/vnd.wolfram.cdf": "CDF", + "application/vnd.wolfram.cdf.text": "CDF", + "application/vnd.wolfram.mathematica.package": "Package", + "application/x-3ds": "3DS", + "application/x-cdf": "NASACDF", + "application/x-eps": "EPS", + "application/x-flac": "FLAC", + "application/x-font-bdf": "BDF", + "application/x-hdf": "HDF", + "application/x-msaccess": "MDB", + "application/x-netcdf": "NetCDF", + "application/x-shockwave-flash": "SWF", + "application/x-tex": "TeX", # Also TeX + "application/xhtml+xml": "XHTML", + "application/xml": "XML", + "application/zip": "ZIP", + "audio/aiff": "AIFF", + "audio/basic": "AU", # Also SND + "audio/midi": "MIDI", + "audio/x-aifc": "AIFF", + "audio/x-aiff": "AIFF", + "audio/x-flac": "FLAC", + "audio/x-wav": "WAV", + "chemical/seq-aa-fasta": "FASTA", + "chemical/seq-na-fasta": "FASTA", + "chemical/seq-na-fastq": "FASTQ", + "chemical/seq-na-genbank": "GenBank", + "chemical/seq-na-sff": "SFF", + "chemical/x-cif": "CIF", + "chemical/x-daylight-smiles": "SMILES", + "chemical/x-hin": "HIN", + "chemical/x-jcamp-dx": "JCAMP-DX", + "chemical/x-mdl-molfile": "MOL", + "chemical/x-mdl-sdf": "SDF", + "chemical/x-mdl-sdfile": "SDF", + "chemical/x-mdl-tgf": "TGF", + "chemical/x-mmcif": "CIF", + "chemical/x-mol2": "MOL2", + "chemical/x-mopac-input": "Table", + "chemical/x-pdb": "PDB", + "chemical/x-xyz": "XYZ", + "image/bmp": "BMP", + "image/eps": "EPS", + "image/fits": "FITS", + "image/gif": "GIF", + "image/jp2": "JPEG2000", + "image/jpeg": "JPEG", + "image/pbm": "PNM", + "image/pcx": "PCX", + "image/pict": "PICT", + "image/png": "PNG", + "image/svg+xml": "SVG", + "image/tga": "TGA", + "image/tiff": "TIFF", + "image/vnd.dxf": "DXF", + "image/vnd.microsoft.icon": "ICO", + "image/x-3ds": "3DS", + "image/x-dxf": "DXF", + "image/x-exr": "OpenEXR", + "image/x-icon": "ICO", + "image/x-ms-bmp": "BMP", + "image/x-pcx": "PCX", + "image/x-portable-anymap": "PNM", + "image/x-portable-bitmap": "PBM", + "image/x-portable-graymap": "PGM", + "image/x-portable-pixmap": "PPM", + "image/x-xbitmap": "XBM", + "model/vrml": "VRML", + "model/x-lwo": "LWO", + "model/x-pov": "POV", + "model/x3d+xml": "X3D", + "text/calendar": "ICS", + "text/comma-separated-values": "CSV", + "text/csv": "CSV", + "text/html": "HTML", + "text/mathml": "MathML", + "text/plain": "Text", + "text/rtf": "RTF", + "text/scriptlet": "SCT", + "text/tab-separated-values": "TSV", + "text/texmacs": "Text", + "text/vnd.graphviz": "DOT", + "text/x-comma-separated-values": "CSV", + "text/x-csrc": "C", + "text/x-tex": "TeX", + "text/x-vcalendar": "VCS", + "text/x-vcard": "VCF", + "text/xml": "XML", + "video/avi": "AVI", + "video/quicktime": "QuickTime", + "video/x-flv": "FLV", + # None: 'Binary', +} + + +def filetype_from_path(path: str) -> Optional[String]: + """Classifies what kind of file `path` is. + A Mathics3 String is return if we can do this and None, if + there was some sort of error, e.g., `path` is not found. + + It does is using a MIME type, even though the path doesn't have to + be something received or transmitted over HTTP. + + MIME types are standardized and do not change, while file + descriptions or WL's codes are not and can change. + """ + + if not osp.exists(path): + return None + + try: + MIME_content_type = from_file(path, mime=True) + return filetype_from_MIME_content(MIME_content_type) + if MIME_content_type in MIMETYPE_TO_SHORTNAME: + short_name = MIMETYPE_TO_SHORTNAME[MIME_content_type] + else: + # Map MIME type to a standard extension using the stdlib + # mimetypes.guess_extension returns things like '.zip' or '.py' + ext = mimetypes.guess_extension(MIME_content_type) + + if ext: + # Clean up the extension (remove trailing dot and uppercase) + short_name = ext.rstrip(".").upper() + else: + short_name = MIME_content_type + + return String(short_name) + + except Exception: + return None + + +def filetype_from_MIME_content(mime_content_name: str) -> Optional[String]: + + if mime_content_name in MIMETYPE_TO_SHORTNAME: + short_name = MIMETYPE_TO_SHORTNAME[mime_content_name] + else: + # Map MIME type to a standard extension using the stdlib + # mimetypes.guess_extension returns things like '.zip' or '.py' + file_extension = mimetypes.guess_extension(mime_content_name) + + if file_extension: + # Clean up the extension (remove trailing dot and uppercase) + short_name = file_extension.rstrip(".").upper() + + return String(short_name) + + +def importer_exporter_options( + available_options, options, builtin_name: str, evaluation +): + stream_options = [] + custom_options = [] + remaining_options = options.copy() + + if available_options and available_options.has_form("List", None): + for name in available_options.elements: + if isinstance(name, String): + py_name = name.get_string_value() + elif isinstance(name, Symbol): + py_name = strip_context(name.get_name()) + else: + py_name = None + + if py_name: + option = get_option(remaining_options, py_name, evaluation, pop=True) + if option is not None: + expr = Expression(SymbolRule, String(py_name), option) + if py_name == "CharacterEncoding": + stream_options.append(expr) + else: + custom_options.append(expr) + + syntax_option = remaining_options.get("System`$OptionSyntax", None) + if syntax_option and syntax_option != Symbol("System`Ignore"): + # warn about unsupported options. + for name, value in remaining_options.items(): + evaluation.message( + builtin_name, + "optx", + Expression(SymbolRule, String(strip_context(name)), value), + strip_context(builtin_name), + ) + + return stream_options, custom_options + + +def eval_Import(findfile, determine_filetype, elements, evaluation, options, data=None): + current_predetermined_out = evaluation.predetermined_out + # Check elements + if elements.has_form("List", None): + elements = elements.get_elements() + else: + elements = [elements] + + for el in elements: + if not isinstance(el, String): + evaluation.message("Import", "noelem", el) + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + + elements = [el.get_string_value() for el in elements] + + # Determine file type + for el in elements: + if el in IMPORTERS.keys(): + filetype = el + elements.remove(el) + break + else: + filetype = determine_filetype() + + if filetype not in IMPORTERS.keys(): + evaluation.message("Import", "fmtnosup", filetype) + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + + # Load the importer + conditionals, default_function, posts, importer_options = IMPORTERS[filetype] + + stream_options, custom_options = importer_exporter_options( + importer_options.get("System`Options"), options, "System`Import", evaluation + ) + + function_channels = importer_options.get("System`FunctionChannels") + + if function_channels is None: + # TODO message + if data is None: + evaluation.message("Import", "emptyfch") + else: + evaluation.message("ImportString", "emptyfch") + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + + default_element = importer_options.get("System`DefaultElement") + if default_element is None: + # TODO message + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + + # Perform the import + defaults = None + + if not elements: + defaults = get_results( + default_function, + findfile, + function_channels, + stream_options, + custom_options, + evaluation, + ) + if defaults is None: + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + elif defaults is SymbolFailed: + return SymbolFailed + if default_element is Symbol("Automatic"): + evaluation.predetermined_out = current_predetermined_out + return ListExpression( + *( + Expression(SymbolRule, String(key), defaults[key]) + for key in defaults.keys() + ) + ) + else: + result = defaults.get(default_element.get_string_value()) + if result is None: + evaluation.message( + "Import", "noelem", default_element, String(filetype) + ) + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + evaluation.predetermined_out = current_predetermined_out + return result + else: + assert len(elements) >= 1 + el = elements[0] + if el == "Elements": + defaults = get_results( + default_function, + findfile, + function_channels, + stream_options, + custom_options, + evaluation, + ) + if defaults is None: + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + # Use set() to remove duplicates + evaluation.predetermined_out = current_predetermined_out + return from_python( + sorted( + set( + list(conditionals.keys()) + + list(defaults.keys()) + + list(posts.keys()) + ) + ) + ) + else: + if el in conditionals.keys(): + result = get_results( + conditionals[el], + findfile, + function_channels, + stream_options, + custom_options, + evaluation, + ) + if result is None: + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + if len(list(result.keys())) == 1 and list(result.keys())[0] == el: + evaluation.predetermined_out = current_predetermined_out + return list(result.values())[0] + elif el in posts.keys(): + # TODO: allow use of conditionals + result = get_results( + posts[el], + findfile, + function_channels, + stream_options, + custom_options, + evaluation, + ) + if result is None: + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + else: + if defaults is None: + defaults = get_results( + default_function, + findfile, + function_channels, + stream_options, + custom_options, + ) + if defaults is None: + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + if el in defaults.keys(): + evaluation.predetermined_out = current_predetermined_out + return defaults[el] + else: + evaluation.message( + "Import", "noelem", from_python(el), String(filetype) + ) + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + + +def get_results( + tmp_function, + findfile, + function_channels, + stream_options, + custom_options, + evaluation, +): + if function_channels == ListExpression(String("FileNames")): + joined_options = list(chain(stream_options, custom_options)) + tmpfile = False + if findfile is None: + tmpfile = True + stream = Expression(SymbolOpenWrite).evaluate(evaluation) + findfile = stream.elements[0] + if data is not None: + Expression(SymbolWriteString, data).evaluate(evaluation) + else: + Expression(SymbolWriteString, String("")).evaluate(evaluation) + eval_Close(stream, evaluation) + import_expression = Expression(tmp_function, findfile, *joined_options) + tmp = import_expression.evaluate(evaluation) + if tmp is SymbolFailed: + return SymbolFailed + if tmpfile: + Expression(SymbolDeleteFile, findfile).evaluate(evaluation) + elif function_channels == ListExpression(String("Streams")): + if findfile is None: + stream = Expression(SymbolStringToStream, data).evaluate(evaluation) + else: + mode = "r" + if options.get("System`BinaryFormat") is SymbolTrue: + if not mode.endswith("b"): + mode += "b" + + encoding_option = options.get("System`CharacterEncoding") + encoding = ( + encoding_option.value if isinstance(encoding_option, String) else None + ) + + stream = eval_Open( + name=findfile, + mode=mode, + stream_type="InputStream", + encoding=encoding, + evaluation=evaluation, + ) + if stream is None: + return + if stream.get_head_name() != "System`InputStream": + evaluation.message("Import", "nffil") + evaluation.predetermined_out = current_predetermined_out + return None + tmp = Expression(tmp_function, stream, *custom_options).evaluate(evaluation) + eval_Close(stream, evaluation) + else: + # TODO message + evaluation.predetermined_out = current_predetermined_out + return SymbolFailed + tmp = tmp.get_elements() + if not all(expr.has_form("Rule", None) for expr in tmp): + evaluation.predetermined_out = current_predetermined_out + return None + + # return {a.get_string_value() : b for a,b in map(lambda x: + # x.get_elements(), tmp)} + evaluation.predetermined_out = current_predetermined_out + return {a.get_string_value(): b for a, b in (x.get_elements() for x in tmp)} From 135ee4c039bda33b271726b4ca7c5a259ee9c7a9 Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 11 May 2026 10:01:57 -0400 Subject: [PATCH 7/8] Finish {Im,Ex}port{,String} refactor part 1 --- mathics/builtin/files_io/importexport.py | 112 +++++++++++++---------- mathics/core/systemsymbols.py | 4 +- mathics/eval/files_io/importexport.py | 50 ++++++++-- 3 files changed, 108 insertions(+), 58 deletions(-) diff --git a/mathics/builtin/files_io/importexport.py b/mathics/builtin/files_io/importexport.py index 33fc89070..cfa0fb2da 100644 --- a/mathics/builtin/files_io/importexport.py +++ b/mathics/builtin/files_io/importexport.py @@ -35,7 +35,16 @@ from mathics.core.list import ListExpression from mathics.core.streams import stream_manager from mathics.core.symbols import Symbol, SymbolNull, SymbolTrue -from mathics.core.systemsymbols import SymbolByteArray, SymbolFailed, SymbolToString +from mathics.core.systemsymbols import ( + SymbolByteArray, + SymbolFailed, + SymbolFileExtension, + SymbolFileFormat, + SymbolFindFile, + SymbolOpenWrite, + SymbolOutputStream, + SymbolToString, +) from mathics.eval.files_io.files import eval_Close from mathics.eval.files_io.importexport import ( IMPORTERS, @@ -1107,11 +1116,16 @@ def eval(self, url: String, elements, evaluation: Evaluation, options={}): # reader (e.g. Import) can access it. so close the file here. os.close(temp_handle) - def determine_filetype(): - return MIMETYPE_TO_SHORTNAME.get(content_type) + def determine_filetype(content_type: str) -> str: + return MIMETYPE_TO_SHORTNAME.get(content_type, "Text") result = eval_Import( - temp_path, determine_filetype, elements, evaluation, options + temp_path, + determine_filetype, + elements, + evaluation, + options, + data=content_type, ) except HTTPError as e: evaluation.message( @@ -1202,11 +1216,11 @@ def eval(self, source, evaluation, options={}): return self.eval_elements(source, ListExpression(), evaluation, options) def eval_element(self, source, element: String, evaluation, options={}): - "Import[filename_, element_String, OptionsPattern[]]" + "Import[source_, element_String, OptionsPattern[]]" return self.eval_elements(source, ListExpression(element), evaluation, options) def eval_elements(self, source, elements, evaluation, options={}): - "Import[filename_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" + "Import[source_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" # Check filename path = source.to_python() if not (isinstance(path, str) and path[0] == path[-1] == '"'): @@ -1220,14 +1234,18 @@ def eval_elements(self, source, elements, evaluation, options={}): evaluation.message("Import", "nffil") return findfile - def determine_filetype(): - return ( - Expression(SymbolFileFormat, findfile) - .evaluate(evaluation=evaluation) - .get_string_value() - ) + data = ( + Expression(SymbolFileFormat, findfile) + .evaluate(evaluation=evaluation) + .get_string_value() + ) - return eval_Import(findfile, determine_filetype, elements, evaluation, options) + def determine_filetype(data: str) -> str: + return data + + return eval_Import( + findfile, determine_filetype, elements, evaluation, options, data=data + ) class ImportString(Builtin): @@ -1270,24 +1288,22 @@ def eval(self, data, evaluation, options={}): "ImportString[data_, OptionsPattern[]]" return self.eval_elements(data, ListExpression(), evaluation, options) - def eval_element(self, source: str, element: String, evaluation, options={}): - "ImportString[source_, element_String, OptionsPattern[]]" + def eval_element(self, data, element: String, evaluation, options={}): + "ImportString[data_, element_String, OptionsPattern[]]" - return self.eval_elements(source, ListExpression(element), evaluation, options) + return self.eval_elements(data, ListExpression(element), evaluation, options) - def eval_elements(self, source, elements, evaluation, options={}): - "ImportString[source_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" - if not (isinstance(source, String)): - evaluation.message("ImportString", "string", source) + def eval_elements(self, data, elements, evaluation, options={}): + "ImportString[data_, elements_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" + if not (isinstance(data, String)): + evaluation.message("ImportString", "string", data) return SymbolFailed - py_source = source.value - - def determine_filetype(): - return filetype_from_MIME_content(py_source) + def determine_filetype(py_data: str) -> str: + return filetype_from_MIME_content(py_data) return eval_Import( - None, determine_filetype, elements, evaluation, options, data=source + None, determine_filetype, elements, evaluation, options, data=data.value ) @@ -1296,11 +1312,11 @@ class Export(Builtin): :WMA link:https://reference.wolfram.com/language/ref/Export.html
-
'Export'["$file$.$ext$", $expr$] +
'Export'["$dest.ext$", $expr$]
exports $expr$ to a file, using the extension $ext$ to determine the format. -
'Export'["$file$", $expr$, "$format$"] -
exports $expr$ to a file in the specified format. +
'Export'["$dest$", $expr$, "$fmt$"] +
exports data $expr$ to a file in the specified format, $fmt$.
'Export'["$file$", $exprs$, $elems$]
exports $exprs$ to a file as elements specified by $elems$. @@ -1361,33 +1377,33 @@ def _infer_form(self, filename, evaluation: Evaluation): # to allow defining specific converters return self._extdict.get(ext) - def eval(self, filename, expr, evaluation, options={}): - "Export[filename_, expr_, OptionsPattern[Export]]" + def eval(self, dest, expr, evaluation, options={}): + "Export[dest_, expr_, OptionsPattern[Export]]" - # Check filename - if not self._check_filename(filename, evaluation): + # Check dest + if not self._check_filename(dest, evaluation): return SymbolFailed # Determine Format - form = self._infer_form(filename, evaluation) + form = self._infer_form(dest, evaluation) if form is None: - evaluation.message("Export", "infer", filename) + evaluation.message("Export", "infer", dest) return SymbolFailed else: - return self.eval_elements(filename, expr, String(form), evaluation, options) + return self.eval_elements(dest, expr, String(form), evaluation, options) - def eval_element(self, filename, expr, element: String, evaluation, options={}): - "Export[filename_, expr_, element_String, OptionsPattern[]]" + def eval_element(self, dest, expr, element: String, evaluation, options={}): + "Export[dest_, expr_, element_String, OptionsPattern[]]" return self.eval_elements( - filename, expr, ListExpression(element), evaluation, options + dest, expr, ListExpression(element), evaluation, options ) - def eval_elements(self, filename, expr, elems, evaluation, options={}): - "Export[filename_, expr_, elems_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" + def eval_elements(self, dest, expr, elems, evaluation, options={}): + "Export[dest_, expr_, elems_List?(AllTrue[#, NotOptionQ]&), OptionsPattern[]]" # Check filename - if not self._check_filename(filename, evaluation): + if not self._check_filename(dest, evaluation): return SymbolFailed # Process elems {comp* format?, elem1*} @@ -1411,9 +1427,9 @@ def eval_elements(self, filename, expr, elems, evaluation, options={}): # Infer format if not present if not found_form: assert format_spec == [] - format_spec = self._infer_form(filename, evaluation) + format_spec = self._infer_form(dest, evaluation) if format_spec is None: - evaluation.message("Export", "infer", filename) + evaluation.message("Export", "infer", dest) evaluation.predetermined_out = current_predetermined_out return SymbolFailed format_spec = [format_spec] @@ -1442,16 +1458,16 @@ def eval_elements(self, filename, expr, elems, evaluation, options={}): elif function_channels == ListExpression(String("FileNames")): exporter_function = Expression( exporter_symbol, - filename, + dest, expr, *list(chain(stream_options, custom_options)), ) res = exporter_function.evaluate(evaluation) elif function_channels == ListExpression(String("Streams")): - stream = Expression(SymbolOpenWrite, filename, *stream_options).evaluate( + stream = Expression(SymbolOpenWrite, dest, *stream_options).evaluate( evaluation ) - if stream.get_head_name() != "System`OutputStream": + if stream.head not in (SymbolOutputStream, SymbolOpenWrite): evaluation.message("Export", "nffil") evaluation.predetermined_out = current_predetermined_out return SymbolFailed @@ -1465,7 +1481,7 @@ def eval_elements(self, filename, expr, elems, evaluation, options={}): eval_Close(stream, evaluation) if res is SymbolNull: evaluation.predetermined_out = current_predetermined_out - return filename + return dest evaluation.predetermined_out = current_predetermined_out return SymbolFailed @@ -1678,7 +1694,7 @@ def eval(self, filename: String, evaluation: Evaluation): ) return findfile - return filetype_from_path(findfile.value) + return String(filetype_from_path(findfile.value)) class B64Decode(Builtin): diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index b6d2d016a..eea8861ee 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -239,7 +239,7 @@ SymbolNumericQ = Symbol("System`NumericQ") SymbolO = Symbol("System`O") SymbolOpacity = Symbol("System`Opacity") -SymbolOpenWrite = Symbol("System`Openwrite") +SymbolOpenWrite = Symbol("System`OpenWrite") SymbolOperate = Symbol("System`Operate") SymbolOptionValue = Symbol("System`OptionValue") SymbolOptional = Symbol("System`Optional") @@ -342,6 +342,7 @@ SymbolStringQ = Symbol("System`StringQ") SymbolStringRiffle = Symbol("System`StringRiffle") SymbolStringSplit = Symbol("System`StringSplit") +SymbolStringToStream = Symbol("System`StringToStream") SymbolStyle = Symbol("System`Style") SymbolStyleBox = Symbol("System`StyleBox") SymbolSubValues = Symbol("System`SubValues") @@ -391,4 +392,5 @@ SymbolWord = Symbol("System`Word") SymbolWordBoundary = Symbol("System`WordBoundary") SymbolWordCharacter = Symbol("System`WordCharacter") +SymbolWriteString = Symbol("System`WriteString") SymbolXor = Symbol("System`Xor") diff --git a/mathics/eval/files_io/importexport.py b/mathics/eval/files_io/importexport.py index ba5048903..516376c22 100644 --- a/mathics/eval/files_io/importexport.py +++ b/mathics/eval/files_io/importexport.py @@ -5,6 +5,7 @@ import mimetypes import os.path as osp +from itertools import chain from typing import Dict, Final, Optional from mathics.core.builtin import String, get_option @@ -12,7 +13,15 @@ from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolTrue, strip_context -from mathics.core.systemsymbols import SymbolFailed, SymbolRule +from mathics.core.systemsymbols import ( + SymbolDeleteFile, + SymbolFailed, + SymbolInputStream, + SymbolOpenWrite, + SymbolRule, + SymbolStringToStream, + SymbolWriteString, +) from mathics.eval.files_io.files import eval_Close, eval_Open IMPORTERS = {} @@ -222,7 +231,7 @@ def filetype_from_path(path: str) -> Optional[String]: return None -def filetype_from_MIME_content(mime_content_name: str) -> Optional[String]: +def filetype_from_MIME_content(mime_content_name: str) -> str: if mime_content_name in MIMETYPE_TO_SHORTNAME: short_name = MIMETYPE_TO_SHORTNAME[mime_content_name] @@ -234,8 +243,10 @@ def filetype_from_MIME_content(mime_content_name: str) -> Optional[String]: if file_extension: # Clean up the extension (remove trailing dot and uppercase) short_name = file_extension.rstrip(".").upper() + else: + return "Text" - return String(short_name) + return short_name def importer_exporter_options( @@ -277,7 +288,14 @@ def importer_exporter_options( return stream_options, custom_options -def eval_Import(findfile, determine_filetype, elements, evaluation, options, data=None): +def eval_Import( + findfile: Optional[String], + determine_filetype, + elements, + evaluation, + options, + data: Optional[str], +): current_predetermined_out = evaluation.predetermined_out # Check elements if elements.has_form("List", None): @@ -300,7 +318,7 @@ def eval_Import(findfile, determine_filetype, elements, evaluation, options, dat elements.remove(el) break else: - filetype = determine_filetype() + filetype = determine_filetype(data) if filetype not in IMPORTERS.keys(): evaluation.message("Import", "fmtnosup", filetype) @@ -342,6 +360,8 @@ def eval_Import(findfile, determine_filetype, elements, evaluation, options, dat stream_options, custom_options, evaluation, + options, + data=data, ) if defaults is None: evaluation.predetermined_out = current_predetermined_out @@ -377,6 +397,8 @@ def eval_Import(findfile, determine_filetype, elements, evaluation, options, dat stream_options, custom_options, evaluation, + options, + data=data, ) if defaults is None: evaluation.predetermined_out = current_predetermined_out @@ -401,6 +423,8 @@ def eval_Import(findfile, determine_filetype, elements, evaluation, options, dat stream_options, custom_options, evaluation, + options, + data=data, ) if result is None: evaluation.predetermined_out = current_predetermined_out @@ -417,6 +441,8 @@ def eval_Import(findfile, determine_filetype, elements, evaluation, options, dat stream_options, custom_options, evaluation, + options, + data=data, ) if result is None: evaluation.predetermined_out = current_predetermined_out @@ -429,6 +455,9 @@ def eval_Import(findfile, determine_filetype, elements, evaluation, options, dat function_channels, stream_options, custom_options, + evaluation, + options, + data=data, ) if defaults is None: evaluation.predetermined_out = current_predetermined_out @@ -446,12 +475,15 @@ def eval_Import(findfile, determine_filetype, elements, evaluation, options, dat def get_results( tmp_function, - findfile, + findfile: Optional[String], function_channels, stream_options, custom_options, evaluation, + options, + data: Optional[str], ): + current_predetermined_out = evaluation.predetermined_out if function_channels == ListExpression(String("FileNames")): joined_options = list(chain(stream_options, custom_options)) tmpfile = False @@ -460,7 +492,7 @@ def get_results( stream = Expression(SymbolOpenWrite).evaluate(evaluation) findfile = stream.elements[0] if data is not None: - Expression(SymbolWriteString, data).evaluate(evaluation) + Expression(SymbolWriteString, String(data)).evaluate(evaluation) else: Expression(SymbolWriteString, String("")).evaluate(evaluation) eval_Close(stream, evaluation) @@ -472,7 +504,7 @@ def get_results( Expression(SymbolDeleteFile, findfile).evaluate(evaluation) elif function_channels == ListExpression(String("Streams")): if findfile is None: - stream = Expression(SymbolStringToStream, data).evaluate(evaluation) + stream = Expression(SymbolStringToStream, String(data)).evaluate(evaluation) else: mode = "r" if options.get("System`BinaryFormat") is SymbolTrue: @@ -493,7 +525,7 @@ def get_results( ) if stream is None: return - if stream.get_head_name() != "System`InputStream": + if stream.head is not SymbolInputStream: evaluation.message("Import", "nffil") evaluation.predetermined_out = current_predetermined_out return None From 54bbd5f5cade7020c7f8b450f82916f94bb77f57 Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 11 May 2026 10:14:09 -0400 Subject: [PATCH 8/8] See if we can get Windows CI working... --- .github/workflows/windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 820be57ec..5fca8651c 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -29,7 +29,7 @@ jobs: # use --force because llvm may already exist, but it also may not exist. # so we will be safe here. Another possibility would be check and install # conditionally. - choco install --force llvm + # choco install --force llvm # choco install tesseract set LLVM_DIR="C:\Program Files\LLVM" - name: Install Mathics3 with Python dependencies