littlefs-python 0.15.0__cp314-cp314-win_amd64.whl

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.
littlefs/__init__.py ADDED
@@ -0,0 +1,494 @@
1
+ import io
2
+ import warnings
3
+ from typing import TYPE_CHECKING, List, Tuple, Iterator, IO, Union, Optional
4
+
5
+ try:
6
+ from importlib_metadata import version, PackageNotFoundError
7
+ except ImportError:
8
+ from importlib.metadata import version, PackageNotFoundError
9
+
10
+
11
+ from . import errors, lfs
12
+ from .lfs import (
13
+ __LFS_DISK_VERSION__,
14
+ __LFS_VERSION__,
15
+ LFSConfig,
16
+ LFSFilesystem,
17
+ LFSFile,
18
+ LFSDirectory,
19
+ LFSFileFlag,
20
+ LFSStat,
21
+ LFSFSStat,
22
+ )
23
+ from .errors import LittleFSError
24
+
25
+ __all__ = [
26
+ "FileHandle",
27
+ "LFSConfig",
28
+ "LFSDirectory",
29
+ "LFSFSStat",
30
+ "LFSFile",
31
+ "LFSFileFlag",
32
+ "LFSFilesystem",
33
+ "LFSStat",
34
+ "LittleFS",
35
+ "LittleFSError",
36
+ "UserContext",
37
+ "UserContextWinDisk",
38
+ "__LFS_DISK_VERSION__",
39
+ "__LFS_VERSION__",
40
+ "errors",
41
+ "lfs",
42
+ ]
43
+
44
+
45
+ try:
46
+ __version__ = version("littlefs-python")
47
+ except PackageNotFoundError:
48
+ # Package not installed
49
+ pass
50
+
51
+ from .context import UserContext, UserContextWinDisk
52
+
53
+ if TYPE_CHECKING:
54
+ from .lfs import LFSStat
55
+
56
+
57
+ class LittleFS:
58
+ """Littlefs file system"""
59
+
60
+ def __init__(self, context: Optional["UserContext"] = None, mount=True, **kwargs) -> None:
61
+ self.cfg = lfs.LFSConfig(context=context, **kwargs)
62
+ self.fs = lfs.LFSFilesystem()
63
+
64
+ if mount:
65
+ try:
66
+ self.mount()
67
+ except errors.LittleFSError:
68
+ self.format()
69
+ self.mount()
70
+
71
+ @property
72
+ def block_count(self) -> int:
73
+ return self.fs.block_count
74
+
75
+ @property
76
+ def used_block_count(self) -> int:
77
+ return lfs.fs_size(self.fs)
78
+
79
+ @property
80
+ def context(self) -> "UserContext":
81
+ """User context of the file system"""
82
+ return self.cfg.user_context
83
+
84
+ def format(self) -> int:
85
+ """Format the underlying buffer"""
86
+ if self.cfg.block_count == 0:
87
+ # ``lfs.format`` looks at cfg's block_count.
88
+ # Cannot autodetect size when formatting.
89
+ raise LittleFSError(LittleFSError.Error.LFS_ERR_INVAL)
90
+ return lfs.format(self.fs, self.cfg)
91
+
92
+ def mount(self) -> int:
93
+ """Mount the underlying buffer"""
94
+ return lfs.mount(self.fs, self.cfg)
95
+
96
+ def unmount(self) -> int:
97
+ """Unmount the underlying buffer"""
98
+ return lfs.unmount(self.fs)
99
+
100
+ def fs_mkconsistent(self) -> int:
101
+ """Attempt to make the filesystem consistent and ready for writing"""
102
+ return lfs.fs_mkconsistent(self.fs)
103
+
104
+ def fs_grow(self, block_count: int) -> int:
105
+ """WARNING: does not modify underlying ``self.context``.
106
+
107
+ Must be done externally.
108
+ """
109
+ if block_count < self.block_count:
110
+ raise ValueError(
111
+ f"Supplied block_count='{block_count}' cannot be smaller than current block_count {self.block_count}"
112
+ )
113
+
114
+ return lfs.fs_grow(self.fs, block_count)
115
+
116
+ def fs_stat(self) -> "LFSFSStat":
117
+ """Get the status of the filesystem"""
118
+ return lfs.fs_stat(self.fs)
119
+
120
+ def fs_gc(self):
121
+ return lfs.fs_gc(self.fs)
122
+
123
+ def open(
124
+ self, fname: str, mode="r", buffering: int = -1, encoding: str = None, errors: str = None, newline: str = None
125
+ ) -> IO:
126
+ """Open a file.
127
+
128
+ :attr:`mode` is an optional string that specifies the mode in which
129
+ the file is opened and is analogous to the built-in :func:`io.open`
130
+ function. Files opened in text mode (default) will take and return
131
+ `str` objects. Files opened in binary mode will take and return
132
+ byte-like objects.
133
+
134
+ Parameters
135
+ ----------
136
+ fname : str
137
+ The path to the file to open.
138
+ mode : str
139
+ Specifies the mode in which the file is opened.
140
+ buffering : int
141
+ Specifies the buffering policy. Pass `0` to disable buffering in
142
+ binary mode.
143
+ encoding : str
144
+ Text encoding to use. (text mode only)
145
+ errors : str
146
+ Specifies how encoding and decoding errors are to be handled. (text mode only)
147
+ newline : str
148
+ Controls how universal newlines mode works. (text mode only)
149
+ """
150
+
151
+ # Parse mode
152
+ creating = False
153
+ reading = False
154
+ writing = False
155
+ appending = False
156
+ updating = False
157
+
158
+ binary = False
159
+ text = False
160
+
161
+ for ch in mode:
162
+ if ch == "x":
163
+ creating = True
164
+ elif ch == "r":
165
+ reading = True
166
+ elif ch == "w":
167
+ writing = True
168
+ elif ch == "a":
169
+ appending = True
170
+ elif ch == "+":
171
+ updating = True
172
+ elif ch == "b":
173
+ binary = True
174
+ elif ch == "t":
175
+ text = True
176
+ else:
177
+ raise ValueError(f"invalid mode: '{mode}'")
178
+
179
+ if text and binary:
180
+ raise ValueError("can't have text and binary mode at once")
181
+
182
+ exclusive_modes = (creating, reading, writing, appending)
183
+
184
+ if sum(int(m) for m in exclusive_modes) > 1:
185
+ raise ValueError("must have exactly one of create/read/write/append mode")
186
+
187
+ if binary:
188
+ if encoding is not None:
189
+ raise ValueError("binary mode doesn't take an encoding argument")
190
+
191
+ if errors is not None:
192
+ raise ValueError("binary mode doesn't take an errors argument")
193
+
194
+ if newline is not None:
195
+ raise ValueError("binary mode doesn't take a newline argument")
196
+
197
+ if buffering == 1:
198
+ msg = (
199
+ "line buffering (buffering=1) isn't supported in "
200
+ "binary mode, the default buffer size will be used"
201
+ )
202
+ warnings.warn(msg, RuntimeWarning)
203
+ buffering = -1
204
+
205
+ try:
206
+ fh = lfs.file_open(self.fs, fname, mode)
207
+ except LittleFSError as e:
208
+ # Try to map to standard Python exceptions
209
+ if e.code == LittleFSError.Error.LFS_ERR_NOENT:
210
+ raise FileNotFoundError from e
211
+ elif e.code == LittleFSError.Error.LFS_ERR_ISDIR:
212
+ raise IsADirectoryError from e
213
+ elif e.code == LittleFSError.Error.LFS_ERR_EXIST:
214
+ raise FileExistsError from e
215
+ else:
216
+ raise e
217
+
218
+ raw = FileHandle(self.fs, fh)
219
+
220
+ line_buffering = False
221
+
222
+ if buffering == 1:
223
+ buffering = -1
224
+ line_buffering = True
225
+
226
+ if buffering < 0:
227
+ buffering = self.cfg.cache_size
228
+
229
+ if buffering == 0:
230
+ if not binary:
231
+ raise ValueError("can't have unbuffered text I/O")
232
+
233
+ return raw
234
+
235
+ if updating:
236
+ buffered = io.BufferedRandom(raw, buffering)
237
+ elif creating or writing or appending:
238
+ buffered = io.BufferedWriter(raw, buffering)
239
+ elif reading:
240
+ buffered = io.BufferedReader(raw, buffering)
241
+ else:
242
+ raise ValueError(f"Unknown mode: '{mode}'")
243
+
244
+ if binary:
245
+ return buffered
246
+
247
+ wrapped = io.TextIOWrapper(buffered, encoding, errors, newline, line_buffering)
248
+
249
+ return wrapped
250
+
251
+ def getattr(self, path: str, typ: Union[str, bytes, int]) -> bytes:
252
+ typ = _typ_to_uint8(typ)
253
+ return lfs.getattr(self.fs, path, typ)
254
+
255
+ def setattr(self, path: str, typ: Union[str, bytes, int], data: bytes) -> None:
256
+ typ = _typ_to_uint8(typ)
257
+ lfs.setattr(self.fs, path, typ, data)
258
+
259
+ def removeattr(self, path: str, typ: Union[str, bytes, int]) -> None:
260
+ typ = _typ_to_uint8(typ)
261
+ lfs.removeattr(self.fs, path, typ)
262
+
263
+ def listdir(self, path=".") -> List[str]:
264
+ """List directory content
265
+
266
+ List the content of a directory. This function uses :meth:`scandir`
267
+ internally. Using :meth:`scandir` might be better if you are
268
+ searching for a specific file or need access to the :class:`littlefs.lfs.LFSStat`
269
+ of the files.
270
+ """
271
+ return [st.name for st in self.scandir(path)]
272
+
273
+ def mkdir(self, path: str) -> int:
274
+ """Create a new directory"""
275
+ try:
276
+ return lfs.mkdir(self.fs, path)
277
+ except errors.LittleFSError as e:
278
+ if e.code == LittleFSError.Error.LFS_ERR_EXIST:
279
+ msg = "[LittleFSError {:d}] Cannot create a file when that file already exists: '{:s}'.".format(
280
+ e.code, path
281
+ )
282
+ raise FileExistsError(msg) from e
283
+ raise
284
+
285
+ def makedirs(self, name: str, exist_ok=False):
286
+ """Recursive directory creation function."""
287
+ parts = [p for p in name.split("/") if p]
288
+ current_name = ""
289
+ for nr, part in enumerate(parts):
290
+ current_name += "/%s" % part
291
+ try:
292
+ self.mkdir(current_name)
293
+ except FileExistsError as e:
294
+ is_last = nr == len(parts) - 1
295
+ if (not is_last) or (is_last and exist_ok):
296
+ continue
297
+ raise e
298
+
299
+ def remove(self, path: str, recursive: bool = False) -> None:
300
+ """Remove a file or directory
301
+
302
+ If the path to remove is a directory, the directory must be empty.
303
+
304
+ Parameters
305
+ ----------
306
+ path : str
307
+ The path to the file or directory to remove.
308
+ recursive: bool
309
+ If ``true`` and ``path`` is a directory, recursively remove all children files/folders.
310
+ """
311
+ try:
312
+ lfs.remove(self.fs, path)
313
+ return
314
+ except errors.LittleFSError as e:
315
+ if e.code == LittleFSError.Error.LFS_ERR_NOENT:
316
+ msg = "[LittleFSError {:d}] No such file or directory: '{:s}'.".format(e.code, path)
317
+ raise FileNotFoundError(msg) from e
318
+ elif e.code == LittleFSError.Error.LFS_ERR_NOTEMPTY and recursive:
319
+ # We want to recursively delete the ``path`` directory.
320
+ # Handled below to reduce amount of logic in ``except`` handler.
321
+ pass
322
+ else:
323
+ raise e
324
+
325
+ # Recursively delete the ``path`` directory
326
+ for elem in self.scandir(path):
327
+ self.remove(path + "/" + elem.name, recursive=True)
328
+ lfs.remove(self.fs, path)
329
+
330
+ def removedirs(self, name):
331
+ """Remove directories recursively
332
+
333
+ This works like :func:`remove` but if the leaf directory
334
+ is empty after the successful removal of :attr:`name`, the
335
+ function tries to recursively remove all parent directories
336
+ which are also empty.
337
+ """
338
+ parts = name.split("/")
339
+ while parts:
340
+ try:
341
+ name = "/".join(parts)
342
+ if not name:
343
+ break
344
+ self.remove("/".join(parts))
345
+ except errors.LittleFSError as e:
346
+ if e.code == LittleFSError.Error.LFS_ERR_NOTEMPTY:
347
+ break
348
+ raise e
349
+ parts.pop()
350
+
351
+ def rename(self, src: str, dst: str) -> int:
352
+ """Rename a file or directory"""
353
+ return lfs.rename(self.fs, src, dst)
354
+
355
+ def rmdir(self, path: str) -> int:
356
+ """Remove a directory
357
+
358
+ This function is an alias for :func:`remove`
359
+ """
360
+ return self.remove(path)
361
+
362
+ def scandir(self, path=".") -> Iterator["LFSStat"]:
363
+ """List directory content"""
364
+ dh = lfs.dir_open(self.fs, path)
365
+ info = lfs.dir_read(self.fs, dh)
366
+ while info:
367
+ if info.name not in [".", ".."]:
368
+ yield info
369
+ info = lfs.dir_read(self.fs, dh)
370
+ lfs.dir_close(self.fs, dh)
371
+
372
+ def stat(self, path: str) -> "LFSStat":
373
+ """Get the status of a file or directory"""
374
+ return lfs.stat(self.fs, path)
375
+
376
+ def unlink(self, path: str) -> int:
377
+ """Remove a file or directory
378
+
379
+ This function is an alias for :func:`remove`.
380
+ """
381
+ return self.remove(path)
382
+
383
+ def walk(self, top: str) -> Iterator[Tuple[str, List[str], List[str]]]:
384
+ """Generate the file names in a directory tree
385
+
386
+ Generate the file and directory names in a directory tree by
387
+ walking the tree top-down. This functions closely resembels the
388
+ behaviour of :func:`os.walk`.
389
+
390
+ Each iteration yields a tuple containing three elements:
391
+
392
+ - The root of the currently processed element
393
+ - A list of directories located in the root
394
+ - A list of filenames located in the root
395
+ """
396
+ files, dirs = [], []
397
+ for elem in self.scandir(top):
398
+ if elem.type == 1:
399
+ files.append(elem.name)
400
+ elif elem.type == 2:
401
+ dirs.append(elem.name)
402
+
403
+ yield top, dirs, files
404
+ for dirname in dirs:
405
+ newtop = "/".join((top, dirname)).replace("//", "/")
406
+ yield from self.walk(newtop)
407
+
408
+
409
+ class FileHandle(io.RawIOBase):
410
+ def __init__(self, fs, fh):
411
+ super().__init__()
412
+
413
+ self.fs = fs
414
+ self.fh = fh
415
+
416
+ def close(self):
417
+ # Base implementation is not used to avoid extra call to flush().
418
+ # LittleFS already flushes the file on close.
419
+
420
+ if not self.closed:
421
+ lfs.file_close(self.fs, self.fh)
422
+ setattr(self, "__IOBase_closed", True)
423
+
424
+ def readable(self):
425
+ return lfs.LFSFileFlag.rdonly in self.fh.flags and not self.closed
426
+
427
+ def writable(self):
428
+ return lfs.LFSFileFlag.wronly in self.fh.flags and not self.closed
429
+
430
+ def seekable(self):
431
+ self._checkClosed()
432
+ return True
433
+
434
+ def seek(self, offset, whence=io.SEEK_SET):
435
+ # Whence constants are reused from the io module. The constants have
436
+ # the same as LFS_SEEK_SET / LFS_SEEK_CUR / LFS_SEEK_END.
437
+ self._checkClosed()
438
+ return lfs.file_seek(self.fs, self.fh, offset, whence)
439
+
440
+ def tell(self):
441
+ self._checkClosed()
442
+ return lfs.file_tell(self.fs, self.fh)
443
+
444
+ def truncate(self, size=None) -> int:
445
+ self._checkClosed()
446
+
447
+ pos = self.tell()
448
+ ret = lfs.file_truncate(self.fs, self.fh, pos)
449
+
450
+ return ret
451
+
452
+ def write(self, data):
453
+ self._checkClosed()
454
+ self._checkWritable()
455
+
456
+ return lfs.file_write(self.fs, self.fh, bytes(data))
457
+
458
+ def readinto(self, b):
459
+ self._checkClosed()
460
+ self._checkReadable()
461
+
462
+ req_len = len(b)
463
+ result = lfs.file_read(self.fs, self.fh, req_len)
464
+ res_len = len(result)
465
+
466
+ b[0:res_len] = result
467
+
468
+ return res_len
469
+
470
+ def readall(self):
471
+ self._checkClosed()
472
+ self._checkReadable()
473
+
474
+ file_size = lfs.file_size(self.fs, self.fh)
475
+ file_pos = self.tell()
476
+ size = file_size - file_pos
477
+
478
+ return lfs.file_read(self.fs, self.fh, size)
479
+
480
+ def flush(self):
481
+ super().flush()
482
+ lfs.file_sync(self.fs, self.fh)
483
+
484
+
485
+ def _typ_to_uint8(typ):
486
+ try:
487
+ out = ord(typ)
488
+ except TypeError:
489
+ out = int(typ)
490
+
491
+ if not (0 <= out <= 255):
492
+ raise ValueError(f"type must be in range [0, 255]")
493
+
494
+ return out