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.
Files changed (48) hide show
  1. {dsff-1.2.4/src/dsff.egg-info → dsff-1.2.5}/PKG-INFO +1 -1
  2. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/internals.md +2 -1
  3. dsff-1.2.5/src/dsff/VERSION.txt +1 -0
  4. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/__common__.py +9 -1
  5. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/__init__.py +3 -3
  6. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/csv.py +2 -2
  7. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/dataset.py +1 -1
  8. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/db.py +1 -1
  9. {dsff-1.2.4 → dsff-1.2.5/src/dsff.egg-info}/PKG-INFO +1 -1
  10. {dsff-1.2.4 → dsff-1.2.5}/src/dsff.egg-info/SOURCES.txt +1 -0
  11. dsff-1.2.5/tests/test_behaviour.py +314 -0
  12. {dsff-1.2.4 → dsff-1.2.5}/tests/test_dsff.py +1 -12
  13. {dsff-1.2.4 → dsff-1.2.5}/tests/test_regressions.py +1 -10
  14. {dsff-1.2.4 → dsff-1.2.5}/tests/utils.py +13 -0
  15. dsff-1.2.4/src/dsff/VERSION.txt +0 -1
  16. {dsff-1.2.4 → dsff-1.2.5}/.coveragerc +0 -0
  17. {dsff-1.2.4 → dsff-1.2.5}/.github/dependabot.yml +0 -0
  18. {dsff-1.2.4 → dsff-1.2.5}/.github/workflows/python-package.yml +0 -0
  19. {dsff-1.2.4 → dsff-1.2.5}/.github/workflows/security.yml +0 -0
  20. {dsff-1.2.4 → dsff-1.2.5}/.gitignore +0 -0
  21. {dsff-1.2.4 → dsff-1.2.5}/.readthedocs.yaml +0 -0
  22. {dsff-1.2.4 → dsff-1.2.5}/LICENSE +0 -0
  23. {dsff-1.2.4 → dsff-1.2.5}/README.md +0 -0
  24. {dsff-1.2.4 → dsff-1.2.5}/SECURITY.md +0 -0
  25. {dsff-1.2.4 → dsff-1.2.5}/docs/coverage.svg +0 -0
  26. {dsff-1.2.4 → dsff-1.2.5}/docs/mkdocs.yml +0 -0
  27. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/api.md +0 -0
  28. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/contributing.md +0 -0
  29. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/img/dsff-from.png +0 -0
  30. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/img/dsff-to.png +0 -0
  31. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/img/icon.png +0 -0
  32. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/img/logo.png +0 -0
  33. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/index.md +0 -0
  34. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/installation.md +0 -0
  35. {dsff-1.2.4 → dsff-1.2.5}/docs/pages/usage.md +0 -0
  36. {dsff-1.2.4 → dsff-1.2.5}/docs/requirements.txt +0 -0
  37. {dsff-1.2.4 → dsff-1.2.5}/pyproject.toml +0 -0
  38. {dsff-1.2.4 → dsff-1.2.5}/pytest.ini +0 -0
  39. {dsff-1.2.4 → dsff-1.2.5}/requirements.txt +0 -0
  40. {dsff-1.2.4 → dsff-1.2.5}/setup.cfg +0 -0
  41. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/__info__.py +0 -0
  42. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/__init__.py +0 -0
  43. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/arff.py +0 -0
  44. {dsff-1.2.4 → dsff-1.2.5}/src/dsff/formats/pa.py +0 -0
  45. {dsff-1.2.4 → dsff-1.2.5}/src/dsff.egg-info/dependency_links.txt +0 -0
  46. {dsff-1.2.4 → dsff-1.2.5}/src/dsff.egg-info/requires.txt +0 -0
  47. {dsff-1.2.4 → dsff-1.2.5}/src/dsff.egg-info/top_level.txt +0 -0
  48. {dsff-1.2.4 → dsff-1.2.5}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsff
3
- Version: 1.2.4
3
+ Version: 1.2.5
4
4
  Summary: DataSet File Format (DSFF)
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -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, ORC, Parquet | table | schema metadata | schema metadata (`__metadata__`) |
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"]): # pragma: no cover
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')")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsff
3
- Version: 1.2.4
3
+ Version: 1.2.5
4
4
  Summary: DataSet File Format (DSFF)
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -39,6 +39,7 @@ src/dsff/formats/dataset.py
39
39
  src/dsff/formats/db.py
40
40
  src/dsff/formats/pa.py
41
41
  tests/__init__.py
42
+ tests/test_behaviour.py
42
43
  tests/test_dsff.py
43
44
  tests/test_regressions.py
44
45
  tests/utils.py
@@ -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(TestCase):
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(TestCase):
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()
@@ -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