dsff 1.2.4__tar.gz → 1.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dsff-1.2.4/src/dsff.egg-info → dsff-1.2.5}/PKG-INFO +1 -1
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/internals.md +2 -1
- dsff-1.2.5/src/dsff/VERSION.txt +1 -0
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/__common__.py +9 -1
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/__init__.py +3 -3
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/csv.py +2 -2
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/dataset.py +1 -1
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/db.py +1 -1
- {dsff-1.2.4 → dsff-1.2.5/src/dsff.egg-info}/PKG-INFO +1 -1
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff.egg-info/SOURCES.txt +1 -0
- dsff-1.2.5/tests/test_behaviour.py +314 -0
- {dsff-1.2.4 → dsff-1.2.5}/tests/test_dsff.py +1 -12
- {dsff-1.2.4 → dsff-1.2.5}/tests/test_regressions.py +1 -10
- {dsff-1.2.4 → dsff-1.2.5}/tests/utils.py +13 -0
- dsff-1.2.4/src/dsff/VERSION.txt +0 -1
- {dsff-1.2.4 → dsff-1.2.5}/.coveragerc +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/.github/dependabot.yml +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/.github/workflows/python-package.yml +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/.github/workflows/security.yml +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/.gitignore +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/.readthedocs.yaml +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/LICENSE +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/README.md +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/SECURITY.md +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/coverage.svg +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/mkdocs.yml +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/api.md +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/contributing.md +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/img/dsff-from.png +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/img/dsff-to.png +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/img/icon.png +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/img/logo.png +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/index.md +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/installation.md +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/pages/usage.md +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/docs/requirements.txt +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/pyproject.toml +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/pytest.ini +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/requirements.txt +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/setup.cfg +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/__info__.py +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/__init__.py +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/arff.py +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/pa.py +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff.egg-info/dependency_links.txt +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff.egg-info/requires.txt +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/src/dsff.egg-info/top_level.txt +0 -0
- {dsff-1.2.4 → dsff-1.2.5}/tests/__init__.py +0 -0
|
@@ -42,6 +42,7 @@ When a `DSFF` is created, every `from_*` and `to_*` function is bound to the ins
|
|
|
42
42
|
| CSV | yes | no (names only on import) | no |
|
|
43
43
|
| SQLite (`db`), missing values as `NULL` | `data` table | `features` table | `metadata` table |
|
|
44
44
|
| FilelessDataset | `data.csv` | `features.json` | `metadata.json` |
|
|
45
|
-
| Feather,
|
|
45
|
+
| Feather, Parquet | table | schema metadata | schema metadata (`__metadata__`) |
|
|
46
|
+
| ORC | table | no (pyarrow does not write it) | no |
|
|
46
47
|
|
|
47
48
|
When exporting to ARFF, the columns listed in `exclude` are dropped, the `label` column (if any) becomes the last attribute, a nominal one named `class`, and each other column becomes a `NUMERIC` attribute if all its values are numbers, a `STRING` one otherwise. String values are quoted, with backslash escapes. When importing, unquoted names, comments before `@RELATION` and quoted values are supported, and only the comments named after an attribute become feature descriptions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.2.5
|
|
@@ -3,6 +3,7 @@ import csv as csvmod
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
from ast import literal_eval
|
|
6
|
+
from functools import wraps
|
|
6
7
|
from io import BytesIO, StringIO
|
|
7
8
|
from os import makedirs, remove
|
|
8
9
|
from os.path import basename, exists, expanduser, isfile, isdir, join, splitext
|
|
@@ -39,13 +40,20 @@ def ensure_str(string):
|
|
|
39
40
|
return str(string)
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
def text_or_path(f):
|
|
43
|
+
def text_or_path(f=None, folders=False):
|
|
44
|
+
""" Decorate a function taking a text, bytes or a path, reading the file in the latter case. A folder is passed as
|
|
45
|
+
a path if folders is True, otherwise the function returns False (as no file content can be checked). """
|
|
46
|
+
if f is None:
|
|
47
|
+
return lambda f: text_or_path(f, folders)
|
|
48
|
+
@wraps(f)
|
|
43
49
|
def _wrapper(txt_path, *args, **kwargs):
|
|
44
50
|
if not isinstance(txt_path, bytes):
|
|
45
51
|
if isinstance(txt_path, str):
|
|
46
52
|
# a string is a path if it points to an existing file or folder (a multiline string is a text)
|
|
47
53
|
if "\n" not in txt_path and exists(p := expanduser(txt_path)):
|
|
48
54
|
if isdir(p):
|
|
55
|
+
if not folders:
|
|
56
|
+
return False
|
|
49
57
|
txt_path = p
|
|
50
58
|
else:
|
|
51
59
|
with open(p, "rb") as fh:
|
|
@@ -305,7 +305,7 @@ class DSFF:
|
|
|
305
305
|
ws, d = self.__wb['data'], data
|
|
306
306
|
if not isinstance(d, (list, tuple)):
|
|
307
307
|
if isfile(expanduser(d)) and splitext(d)[1] == ".csv":
|
|
308
|
-
with open(expanduser(d)) as f:
|
|
308
|
+
with open(expanduser(d), newline='') as f:
|
|
309
309
|
d = [r for r in csvmod.reader(f, delimiter=CSV_DELIMITER)]
|
|
310
310
|
else:
|
|
311
311
|
raise BadInputData("'data' is not a list")
|
|
@@ -412,14 +412,14 @@ class DSFF:
|
|
|
412
412
|
return p
|
|
413
413
|
|
|
414
414
|
@staticmethod
|
|
415
|
-
@text_or_path
|
|
415
|
+
@text_or_path(folders=True)
|
|
416
416
|
def format(text):
|
|
417
417
|
for k, func in globals().items():
|
|
418
418
|
if k.startswith("is_") and func(text):
|
|
419
419
|
return k.split("_", 1)[1]
|
|
420
420
|
|
|
421
421
|
@staticmethod
|
|
422
|
-
@text_or_path
|
|
422
|
+
@text_or_path(folders=True)
|
|
423
423
|
def is_valid_format(text):
|
|
424
424
|
return DSFF.format(text) is not None
|
|
425
425
|
|
|
@@ -27,7 +27,7 @@ def is_csv(text, **kw):
|
|
|
27
27
|
def load_csv(path, exclude=DEFAULT_EXCL, **kw):
|
|
28
28
|
""" Load a CSV file as a dictionary with data, features and metadata. """
|
|
29
29
|
data = {'metadata': {}}
|
|
30
|
-
with open(expanduser(path)) as f:
|
|
30
|
+
with open(expanduser(path), newline='') as f:
|
|
31
31
|
data['data'] = [r for r in csvmod.reader(f, delimiter=CSV_DELIMITER)]
|
|
32
32
|
data['features'] = {h: "" for h in data['data'][0] if h not in exclude and h != TARGET_NAME}
|
|
33
33
|
return data
|
|
@@ -35,7 +35,7 @@ def load_csv(path, exclude=DEFAULT_EXCL, **kw):
|
|
|
35
35
|
|
|
36
36
|
def to_csv(dsff, path=None, text=False, **kw):
|
|
37
37
|
""" Create a CSV from the data worksheet, saved as a file or output as a string. """
|
|
38
|
-
with (StringIO() if text else open(path, 'w+')) as f:
|
|
38
|
+
with (StringIO() if text else open(path, 'w+', newline='')) as f:
|
|
39
39
|
writer = csvmod.writer(f, delimiter=";")
|
|
40
40
|
for row in dsff.data:
|
|
41
41
|
writer.writerow(row)
|
|
@@ -52,7 +52,7 @@ def to_dataset(dsff, path=None, **kw):
|
|
|
52
52
|
makedirs(path, exist_ok=True)
|
|
53
53
|
# handle data first
|
|
54
54
|
dsff.logger.debug("> making data.csv...")
|
|
55
|
-
with open(join(path, "data.csv"), 'w+') as f:
|
|
55
|
+
with open(join(path, "data.csv"), 'w+', newline='') as f:
|
|
56
56
|
writer = csvmod.writer(f, delimiter=";")
|
|
57
57
|
for cells in dsff['data'].rows:
|
|
58
58
|
writer.writerow([dsff._DSFF__eval(c.value) for c in cells])
|
|
@@ -49,7 +49,7 @@ def load_db(path, **kw):
|
|
|
49
49
|
# list tables
|
|
50
50
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
|
51
51
|
tables = [table[0] for table in cursor.fetchall()]
|
|
52
|
-
if not all(t in tables for t in ["data", "features", "metadata"]):
|
|
52
|
+
if not all(t in tables for t in ["data", "features", "metadata"]):
|
|
53
53
|
raise BadInputData("The target SQLDB does not have the right format")
|
|
54
54
|
# import data
|
|
55
55
|
cursor.execute("PRAGMA table_info('data')")
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: UTF-8 -*-
|
|
3
|
+
"""DSFF behaviour tests.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
import sqlite3
|
|
8
|
+
|
|
9
|
+
from .utils import *
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DATA = [["hash", "a", "b", "c", "label"], ["h1", 1, 2.5, True, "upx"], ["h2", 3, 0.5, False, "?"]]
|
|
13
|
+
FEATURES = {'a': "integer feature", 'b': "float feature", 'c': "boolean feature"}
|
|
14
|
+
METADATA = {'source': "test", 'counts': {'upx': 1}}
|
|
15
|
+
ARROW = ["feather", "orc", "parquet"] if PYARROW else []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make(name="test", data=DATA, features=FEATURES, metadata=METADATA):
|
|
19
|
+
with DSFF(name, 'w') as f:
|
|
20
|
+
f.write(data, features, metadata)
|
|
21
|
+
return f"{name}.dsff"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestModes(DsffTestCase):
|
|
25
|
+
def test_default_mode(self):
|
|
26
|
+
self.assertEqual(DSFF().mode, "w")
|
|
27
|
+
self.assertEqual(DSFF(INMEMORY).mode, "w")
|
|
28
|
+
self.assertEqual(DSFF(make()).mode, "r")
|
|
29
|
+
|
|
30
|
+
def test_save_method(self):
|
|
31
|
+
path = make()
|
|
32
|
+
self.assertFalse(hasattr(DSFF(path, 'r'), "save"))
|
|
33
|
+
for mode in ["r+", "w", "w+"]:
|
|
34
|
+
self.assertTrue(hasattr(DSFF(path, mode), "save"))
|
|
35
|
+
path = make()
|
|
36
|
+
|
|
37
|
+
def test_read_write(self):
|
|
38
|
+
# r+ keeps the content and saves the changes
|
|
39
|
+
with DSFF(path := make(), 'r+') as f:
|
|
40
|
+
self.assertEqual(f.data, DATA)
|
|
41
|
+
f['note'] = "updated"
|
|
42
|
+
self.assertEqual(DSFF(path)['note'], "updated")
|
|
43
|
+
# w truncates an existing file when opened
|
|
44
|
+
with DSFF(path, 'w') as f:
|
|
45
|
+
self.assertEqual(f.data, [])
|
|
46
|
+
self.assertFalse(os.path.exists(path))
|
|
47
|
+
|
|
48
|
+
def test_save(self):
|
|
49
|
+
f = DSFF(path := make(), 'r+')
|
|
50
|
+
f['note'] = "saved"
|
|
51
|
+
f.save()
|
|
52
|
+
f.close()
|
|
53
|
+
self.assertEqual(DSFF(path).metadata['note'], "saved")
|
|
54
|
+
|
|
55
|
+
def test_in_memory(self):
|
|
56
|
+
with DSFF(INMEMORY) as f:
|
|
57
|
+
f.write(DATA, FEATURES, METADATA)
|
|
58
|
+
self.assertEqual(f.data, DATA)
|
|
59
|
+
self.assertEqual(f.path, INMEMORY)
|
|
60
|
+
self.assertEqual(os.listdir(), [])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestProperties(DsffTestCase):
|
|
64
|
+
def test_properties(self):
|
|
65
|
+
logger = logging.getLogger("test")
|
|
66
|
+
with DSFF(make(), 'r+', logger=logger) as f:
|
|
67
|
+
self.assertEqual(f.name, "test")
|
|
68
|
+
self.assertEqual(f.path, "test.dsff")
|
|
69
|
+
self.assertEqual(f.headers, list(FEATURES.keys()))
|
|
70
|
+
self.assertEqual(f.features, FEATURES)
|
|
71
|
+
self.assertEqual(f.metadata, METADATA)
|
|
72
|
+
self.assertEqual(len(f), len(DATA))
|
|
73
|
+
self.assertIs(f.logger, logger)
|
|
74
|
+
f.name = "renamed"
|
|
75
|
+
self.assertEqual(f.name, "renamed")
|
|
76
|
+
f._DSFF__change = True
|
|
77
|
+
# the name is saved as the title
|
|
78
|
+
self.assertEqual(DSFF("test.dsff")['title'], "renamed")
|
|
79
|
+
self.assertEqual(DSFF("other", 'w').path, "other.dsff")
|
|
80
|
+
|
|
81
|
+
def test_items(self):
|
|
82
|
+
with DSFF(make(), 'r+') as f:
|
|
83
|
+
self.assertIsInstance(f['data'], Worksheet)
|
|
84
|
+
self.assertIsInstance(f['features'], Worksheet)
|
|
85
|
+
# standard properties
|
|
86
|
+
self.assertIsInstance(f['created'], datetime)
|
|
87
|
+
f['creator'] = "someone"
|
|
88
|
+
self.assertEqual(f['creator'], "someone")
|
|
89
|
+
# 'description' holds the metadata, hence it is a metadata key
|
|
90
|
+
f['description'] = "a dataset"
|
|
91
|
+
self.assertEqual(f['description'], "a dataset")
|
|
92
|
+
self.assertEqual(f.metadata['description'], "a dataset")
|
|
93
|
+
self.assertEqual(f['source'], "test")
|
|
94
|
+
self.assertRaises(KeyError, f.__getitem__, "missing_key")
|
|
95
|
+
for name in ["data", "features"]:
|
|
96
|
+
self.assertRaises(ValueError, f.__setitem__, name, None)
|
|
97
|
+
self.assertEqual(DSFF("test.dsff")['creator'], "someone")
|
|
98
|
+
|
|
99
|
+
def test_creator(self):
|
|
100
|
+
from getpass import getuser
|
|
101
|
+
self.assertEqual(DSFF(make())['creator'], getuser())
|
|
102
|
+
|
|
103
|
+
def test_data_only(self):
|
|
104
|
+
# a DSFF without features is valid
|
|
105
|
+
with DSFF("test", 'w') as f:
|
|
106
|
+
f.write(DATA)
|
|
107
|
+
with DSFF("test.dsff") as f:
|
|
108
|
+
self.assertEqual(f.data, DATA)
|
|
109
|
+
self.assertEqual(f.features, {})
|
|
110
|
+
self.assertEqual(f.metadata, {})
|
|
111
|
+
|
|
112
|
+
def test_exceptions(self):
|
|
113
|
+
self.assertTrue(issubclass(BadDsffFile, OSError))
|
|
114
|
+
self.assertTrue(issubclass(BadInputData, ValueError))
|
|
115
|
+
self.assertTrue(issubclass(EmptyDsffFile, ValueError))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestWrite(DsffTestCase):
|
|
119
|
+
def test_from_files(self):
|
|
120
|
+
os.makedirs("ds")
|
|
121
|
+
with open("ds/data.csv", 'w') as f:
|
|
122
|
+
f.write("hash;a;label\nh1;1;upx\n")
|
|
123
|
+
with open("ds/features.json", 'w') as f:
|
|
124
|
+
json.dump({'a': "A"}, f)
|
|
125
|
+
with open("ds/metadata.json", 'w') as f:
|
|
126
|
+
json.dump({'m': 1}, f)
|
|
127
|
+
with DSFF("test", 'w') as f:
|
|
128
|
+
f.write("ds/data.csv", "ds/features.json", "ds/metadata.json")
|
|
129
|
+
self.assertEqual((f.data, f.features, f.metadata), ([["hash", "a", "label"], ["h1", 1, "upx"]], {'a': "A"},
|
|
130
|
+
{'m': 1}))
|
|
131
|
+
# a dataset folder provides the three files and the name
|
|
132
|
+
with DSFF() as f:
|
|
133
|
+
f.write("ds")
|
|
134
|
+
self.assertEqual(f.path, "ds.dsff")
|
|
135
|
+
# files with other names are rejected
|
|
136
|
+
os.rename("ds/features.json", "ds/other.json")
|
|
137
|
+
with DSFF() as f:
|
|
138
|
+
self.assertRaises(BadInputData, f.write, None, "ds/other.json")
|
|
139
|
+
self.assertRaises(BadInputData, f.write, None, None, "ds/other.json")
|
|
140
|
+
self.assertRaises(BadInputData, f.write, "ds/other.json")
|
|
141
|
+
|
|
142
|
+
def test_missing_token(self):
|
|
143
|
+
with DSFF("test", 'w') as f:
|
|
144
|
+
f.write([["a", "label"], [None, "x"]], missing="NA")
|
|
145
|
+
self.assertEqual(f.data, [["a", "label"], ["NA", "x"]])
|
|
146
|
+
|
|
147
|
+
def test_empty(self):
|
|
148
|
+
with DSFF(INMEMORY) as f:
|
|
149
|
+
for fmt in ["arff", "csv", "db", "dataset"] + ARROW:
|
|
150
|
+
self.assertRaises(EmptyDsffFile, getattr(f, f"to_{fmt}"))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestConversions(DsffTestCase):
|
|
154
|
+
def assertRoundTrip(self, fmt, data=DATA, features=FEATURES, metadata=METADATA):
|
|
155
|
+
with DSFF(make()) as f:
|
|
156
|
+
getattr(f, f"to_{fmt}")()
|
|
157
|
+
with DSFF("back", 'w') as f:
|
|
158
|
+
getattr(f, f"from_{fmt}")("test")
|
|
159
|
+
self.assertEqual(f.data, data)
|
|
160
|
+
self.assertEqual(f.features, features)
|
|
161
|
+
self.assertEqual(f.metadata, metadata)
|
|
162
|
+
|
|
163
|
+
def test_arff(self):
|
|
164
|
+
# the excluded columns (here 'hash') are dropped
|
|
165
|
+
self.assertRoundTrip("arff", [r[1:] for r in DATA])
|
|
166
|
+
self.assertEqual(DSFF("back.dsff")['title'], "test")
|
|
167
|
+
|
|
168
|
+
def test_csv(self):
|
|
169
|
+
# CSV only holds the data ; feature names come from the headers
|
|
170
|
+
self.assertRoundTrip("csv", features={'a': "", 'b': "", 'c': ""}, metadata={})
|
|
171
|
+
|
|
172
|
+
def test_dataset(self):
|
|
173
|
+
self.assertRoundTrip("dataset")
|
|
174
|
+
self.assertEqual(sorted(os.listdir("test")), ["data.csv", "features.json", "metadata.json"])
|
|
175
|
+
|
|
176
|
+
def test_db(self):
|
|
177
|
+
self.assertRoundTrip("db")
|
|
178
|
+
|
|
179
|
+
def test_pyarrow(self):
|
|
180
|
+
for fmt in ARROW:
|
|
181
|
+
with self.subTest(fmt=fmt):
|
|
182
|
+
# pyarrow's ORC writer does not keep the schema metadata
|
|
183
|
+
self.assertRoundTrip(fmt, **({'features': {}, 'metadata': {}} if fmt == "orc" else {}))
|
|
184
|
+
|
|
185
|
+
def test_text_output(self):
|
|
186
|
+
with DSFF(make()) as f:
|
|
187
|
+
for fmt in ["arff", "csv"]:
|
|
188
|
+
getattr(f, f"to_{fmt}")()
|
|
189
|
+
with open(f"test.{fmt}", newline='') as fh:
|
|
190
|
+
self.assertEqual(getattr(f, f"to_{fmt}")(text=True), fh.read())
|
|
191
|
+
for fmt in ARROW:
|
|
192
|
+
self.assertIsInstance(getattr(f, f"to_{fmt}")(text=True), bytes)
|
|
193
|
+
sql = f.to_db(text=True)
|
|
194
|
+
conn = sqlite3.connect(":memory:")
|
|
195
|
+
conn.executescript(sql)
|
|
196
|
+
self.assertEqual(conn.execute("SELECT COUNT(*) FROM data;").fetchone(), (len(DATA) - 1, ))
|
|
197
|
+
conn.close()
|
|
198
|
+
|
|
199
|
+
def test_paths(self):
|
|
200
|
+
with DSFF(make()) as f:
|
|
201
|
+
f.to_csv("out")
|
|
202
|
+
f.to_csv("other.csv")
|
|
203
|
+
f.to_dataset("folder.v1")
|
|
204
|
+
for path in ["out.csv", "other.csv", "folder.v1/data.csv"]:
|
|
205
|
+
self.assertTrue(os.path.isfile(path))
|
|
206
|
+
|
|
207
|
+
def test_format(self):
|
|
208
|
+
with DSFF(make()) as f:
|
|
209
|
+
for fmt in ["arff", "csv", "db", "dataset"] + ARROW:
|
|
210
|
+
getattr(f, f"to_{fmt}")()
|
|
211
|
+
for fmt in ["arff", "csv", "db"] + ARROW:
|
|
212
|
+
with self.subTest(fmt=fmt):
|
|
213
|
+
self.assertEqual(DSFF.format(f"test.{fmt}"), fmt)
|
|
214
|
+
self.assertEqual(DSFF.format("test"), "dataset")
|
|
215
|
+
self.assertEqual(DSFF.format("test.dsff"), "dsff")
|
|
216
|
+
with open("test.dsff", 'rb') as f:
|
|
217
|
+
self.assertEqual(DSFF.format(f.read()), "dsff")
|
|
218
|
+
self.assertFalse(DSFF.is_valid_format(b"\x00\x01\x02"))
|
|
219
|
+
self.assertTrue(DSFF.is_valid_format("test.dsff"))
|
|
220
|
+
|
|
221
|
+
def test_load(self):
|
|
222
|
+
with DSFF(make()) as f:
|
|
223
|
+
for fmt in ["arff", "csv", "db", "dataset"] + ARROW:
|
|
224
|
+
getattr(f, f"to_{fmt}")()
|
|
225
|
+
for fmt in ["arff", "csv", "db", "dataset"] + ARROW:
|
|
226
|
+
with self.subTest(fmt=fmt):
|
|
227
|
+
d = globals()[f'load_{fmt}']("test" if fmt == "dataset" else f"test.{fmt}")
|
|
228
|
+
self.assertEqual(sorted(d.keys()), ["data", "features", "metadata"])
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class TestArff(DsffTestCase):
|
|
232
|
+
def test_parsing(self):
|
|
233
|
+
with open("test.arff", 'w') as f:
|
|
234
|
+
f.write("@RELATION \"my data\"\n@ATTRIBUTE bin NUMERIC\n@ATTRIBUTE txt STRING\n"
|
|
235
|
+
"@ATTRIBUTE target {a,b}\n@DATA\n1, -, a\n0, 'x', NA\n")
|
|
236
|
+
d = load_arff("test.arff", target="target", missing="NA")
|
|
237
|
+
# binary columns become booleans, '-' an empty string and the missing token None
|
|
238
|
+
self.assertEqual(d['data'], [["bin", "txt", "target"], ["True", "", "a"], ["False", "x", None]])
|
|
239
|
+
self.assertEqual(d['features'], {'bin': "", 'txt': ""})
|
|
240
|
+
with DSFF("test2", 'w') as f:
|
|
241
|
+
f.from_arff("test.arff")
|
|
242
|
+
self.assertEqual(f['title'], "my data")
|
|
243
|
+
|
|
244
|
+
def test_class_renamed(self):
|
|
245
|
+
d = load_arff_text("@RELATION r\n@ATTRIBUTE a NUMERIC\n@ATTRIBUTE class {x}\n@DATA\n1,x\n", target="y")
|
|
246
|
+
self.assertEqual(d['data'][0], ["a", "y"])
|
|
247
|
+
|
|
248
|
+
def test_errors(self):
|
|
249
|
+
base = "@RELATION r\n@ATTRIBUTE a NUMERIC\n@DATA\n1\n"
|
|
250
|
+
for bad in ["@ATTRIBUTE a NUMERIC\n@DATA\n1\n", # no @RELATION
|
|
251
|
+
"@RELATION r\n@DATA\n1\n", # no @ATTRIBUTE
|
|
252
|
+
"@RELATION r\n@ATTRIBUTE a\n@DATA\n1\n", # no type
|
|
253
|
+
"@RELATION r\n@ATTRIBUTE a NUMERIC\n1\n", # no @DATA
|
|
254
|
+
base + "@ATTRIBUTE b NUMERIC\n", # @ATTRIBUTE after @DATA
|
|
255
|
+
base + "1, 2\n", # wrong number of columns
|
|
256
|
+
"@RELATION r\n@ATTRIBUTE a NUMERIC\n@DATA\n'a' 'b'\n", # garbage after a quoted value
|
|
257
|
+
b"\xff\xfe"]: # not text
|
|
258
|
+
with self.subTest(bad=bad):
|
|
259
|
+
self.assertFalse(is_arff(bad))
|
|
260
|
+
self.assertTrue(is_arff(base))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TestDb(DsffTestCase):
|
|
264
|
+
def test_bad_database(self):
|
|
265
|
+
conn = sqlite3.connect("bad.db")
|
|
266
|
+
conn.execute("CREATE TABLE other (a TEXT);")
|
|
267
|
+
conn.commit()
|
|
268
|
+
conn.close()
|
|
269
|
+
self.assertTrue(is_db("bad.db"))
|
|
270
|
+
self.assertRaises(BadInputData, load_db, "bad.db")
|
|
271
|
+
|
|
272
|
+
def test_types(self):
|
|
273
|
+
with DSFF(make()) as f:
|
|
274
|
+
f.to_db()
|
|
275
|
+
conn = sqlite3.connect("test.db")
|
|
276
|
+
schema = conn.execute("SELECT sql FROM sqlite_master WHERE name='data';").fetchone()[0]
|
|
277
|
+
conn.close()
|
|
278
|
+
for field in ['"hash" TEXT PRIMARY KEY', '"a" INTEGER', '"b" REAL', '"c" INTEGER', '"label" TEXT']:
|
|
279
|
+
self.assertIn(field, schema)
|
|
280
|
+
|
|
281
|
+
def test_primary_index(self):
|
|
282
|
+
with DSFF(make()) as f:
|
|
283
|
+
self.assertIn('"a" INTEGER PRIMARY KEY', f.to_db(text=True, primary_index=1))
|
|
284
|
+
self.assertIn('CREATE TABLE data ("hash" TEXT, "a" INTEGER,', f.to_db(text=True, primary_index=None))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestCsv(DsffTestCase):
|
|
288
|
+
def test_detection(self):
|
|
289
|
+
self.assertTrue(is_csv("a;b\n1;2\n"))
|
|
290
|
+
self.assertTrue(is_csv(b"a,b\n1,2\n"))
|
|
291
|
+
self.assertFalse(is_csv(b"\xff\xfe\x00"))
|
|
292
|
+
# a folder is not a CSV, even if its name looks like one
|
|
293
|
+
os.makedirs("folder")
|
|
294
|
+
self.assertFalse(is_csv("folder"))
|
|
295
|
+
self.assertRaises(ValueError, is_csv, 1)
|
|
296
|
+
|
|
297
|
+
def test_exclude(self):
|
|
298
|
+
with open("test.csv", 'w') as f:
|
|
299
|
+
f.write("hash;size;a;label\nh;1;2;x\n")
|
|
300
|
+
self.assertEqual(load_csv("test.csv")['features'], {'a': ""})
|
|
301
|
+
self.assertEqual(load_csv("test.csv", exclude=("a", ))['features'], {'hash': "", 'size': ""})
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def load_arff_text(text, **kw):
|
|
305
|
+
with open("text.arff", 'w') as f:
|
|
306
|
+
f.write(text)
|
|
307
|
+
return load_arff("text.arff", **kw)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TestCommon(DsffTestCase):
|
|
311
|
+
def test_ensure_str(self):
|
|
312
|
+
self.assertEqual(ensure_str(b"text"), "text")
|
|
313
|
+
self.assertEqual(ensure_str("text"), "text")
|
|
314
|
+
self.assertEqual(ensure_str(1), "1")
|
|
@@ -6,18 +6,7 @@
|
|
|
6
6
|
from .utils import *
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class TestDsff(
|
|
10
|
-
@classmethod
|
|
11
|
-
def tearDownClass(cls):
|
|
12
|
-
rmdir(TEST_BASENAME)
|
|
13
|
-
rmfile(TEST)
|
|
14
|
-
rmfile("undefined.dsff")
|
|
15
|
-
for ext in [".arff", ".csv", ".db"]:
|
|
16
|
-
rmfile(TEST_BASENAME + ext)
|
|
17
|
-
|
|
18
|
-
def tearDown(self):
|
|
19
|
-
rmfile(TEST)
|
|
20
|
-
|
|
9
|
+
class TestDsff(DsffTestCase):
|
|
21
10
|
def test_dsff_modes(self):
|
|
22
11
|
# path not set, shall not work with 'r+?' or a bad mode
|
|
23
12
|
self.assertRaises(ValueError, DSFF, None, "BAD_MODE")
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""
|
|
6
6
|
import sqlite3
|
|
7
|
-
import tempfile
|
|
8
7
|
|
|
9
8
|
from .utils import *
|
|
10
9
|
|
|
@@ -13,15 +12,7 @@ DATA = [["hash", "a", "b", "label"], ["x", 1, 2.5, "upx"], ["y", 3, None, "?"]]
|
|
|
13
12
|
FEATURES = {'a': "feature A", 'b': "feature B"}
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
class TestRegressions(
|
|
17
|
-
def setUp(self):
|
|
18
|
-
self._cwd, self._tmp = os.getcwd(), tempfile.TemporaryDirectory()
|
|
19
|
-
os.chdir(self._tmp.name)
|
|
20
|
-
|
|
21
|
-
def tearDown(self):
|
|
22
|
-
os.chdir(self._cwd)
|
|
23
|
-
self._tmp.cleanup()
|
|
24
|
-
|
|
15
|
+
class TestRegressions(DsffTestCase):
|
|
25
16
|
def test_metadata_values(self):
|
|
26
17
|
# booleans, None and apostrophes in metadata shall not make the file unreadable
|
|
27
18
|
with DSFF("test", 'w') as f:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import hashlib
|
|
3
3
|
import os
|
|
4
4
|
import random
|
|
5
|
+
import tempfile
|
|
5
6
|
import zipfile
|
|
6
7
|
from contextlib import contextmanager
|
|
7
8
|
from datetime import datetime
|
|
@@ -16,6 +17,7 @@ TEST_NAME = "example-dataset"
|
|
|
16
17
|
TEST = f".{TEST_NAME}.dsff"
|
|
17
18
|
TEST_BASENAME = f".{TEST_NAME}"
|
|
18
19
|
|
|
20
|
+
random.seed(42) # reproducible test data
|
|
19
21
|
TEST_DATA = [
|
|
20
22
|
["hash", "field1", "field2", "label"],
|
|
21
23
|
]
|
|
@@ -85,6 +87,17 @@ def rmfile(path):
|
|
|
85
87
|
pass
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
class DsffTestCase(TestCase):
|
|
91
|
+
""" Test case running each test in its own temporary folder. """
|
|
92
|
+
def setUp(self):
|
|
93
|
+
self._cwd, self._tmp = os.getcwd(), tempfile.TemporaryDirectory()
|
|
94
|
+
os.chdir(self._tmp.name)
|
|
95
|
+
|
|
96
|
+
def tearDown(self):
|
|
97
|
+
os.chdir(self._cwd)
|
|
98
|
+
self._tmp.cleanup()
|
|
99
|
+
|
|
100
|
+
|
|
88
101
|
class Unserializable:
|
|
89
102
|
test = 1
|
|
90
103
|
TEST_OBJECT = Unserializable()
|
dsff-1.2.4/src/dsff/VERSION.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.2.4
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|