From 226132dd481d419e73b94554037abbf08fef5201 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Wed, 21 May 2014 10:18:25 +0200 Subject: [PATCH] Initial commit --- README.md | 190 ++++++++++++++++++++++++++++++++++++++++++ filething/__init__.py | 131 +++++++++++++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 README.md create mode 100644 filething/__init__.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb40a5a --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# filething + +Filesystem operations are one of those things in the Python standard library that just kind of suck. + +`filething` is a thin light-weight wrapper library, to make filesystem operations in Python suck less. It's primarily meant for read-only stuff, and doesn't do anything to set file attributes and so on. + +## License + +[WTFPL](http://wtfpl.net/) or [CC0](https://creativecommons.org/publicdomain/zero/1.0/), your choice. + +## Platforms + +Theoretically cross-platform. Then again, Windows will probably be Windows and thus it may break there. I have no idea, I don't use Windows. All code is pure Python, anyway. + +## Installation + +`pip install filething` + +Done. You'll need `pip`, of course. + +## Usage + +First of all, import `filething`. + +```python +import filething +``` + +Then there's a bunch of stuff you can do. To start working with a directory or file, create a `Directory` or `File` object respectively. + +To create a new `Directory` object: + +```python +some_dir = filething.Directory("/path/to/directory") +``` + +To create a new `File` object: + +```python +some_file = filething.File("/path/to/file") +``` + +Entering a non-existent (or inaccessible) path will result in a `filething.FilesystemException` being raised. + +### Directory and File objects + +The `Directory` and `File` classes have some things in common. + +#### Attributes + +The following attributes are automatically set on both `Directory` and `File` objects: + +* `path`: The path of the file or directory. +* `name`: The name of the file or directory. This is generally the part after the last slash. +* `is_symlink`: Boolean. Whether the file/directory is a symlink or not. + +#### Directory/File information + +To learn more about a directory or file, you can use the `stat` or `symlink_stat` methods. + +`stat` will give you the metadata for a file or directory, resolving a symlink if necessary. `symlink_stat` only applies to symbolic links, and gives you metadata about the symlink itself. + +Trying to use `symlink_stat` on something that isn't a symlink, will raise a `filething.FilesystemException`. The `symlink_stat` function returns data in the same format as `stat`. The below applies to both. + +```python +metadata = some_file.stat() +``` + +By default, the `stat` data will be returned as a custom `Attributes` object, with more human-meaningful names than what Python provides. The below is a list of available attributes, with a description (and their original name in `os.stat` in parentheses). I'll assume that the metadata is stored in a `metadata` variable, as above. + +As with Python's `os.stat`, the exact meaning and accuracy of `lastmodified`, `lastaccessed` and `ctime` differ across platforms and filesystems. + +Cross-platform (more-or-less): + +* **metadata.size** (*st_size*): size of file, in bytes +* **metadata.lastaccessed** (*st_atime*): time of most recent access +* **metadata.lastmodified** (*st_mtime*): time of most recent content modification +* **metadata.uid** (*st_uid*): user id of owner +* **metadata.gid** (*st_gid*): group id of owner + +* **metadata.mode** (*st_mode*): protection bits +* **metadata.inode** (*st_ino*): inode number +* **metadata.device** (*st_dev*): device +* **metadata.links** (*st_nlink*): number of hard links +* **metadata.ctime** (*st_ctime*): platform dependent; time of most recent metadata change on Unix, or the time of creation on Windows + +On some UNIX-like (eg. Linux): + +* **metadata.blockcount** (*st_blocks*): number of 512-byte blocks allocated for file +* **metadata.blocksize** (*st_blksize*): filesystem blocksize for efficient file system I/O +* **metadata.devicetype** (*st_rdev*): type of device if an inode device +* **metadata.userflags** (*st_flags*): user defined flags for file + +On some other UNIX-like (eg. FreeBSD): + +* **metadata.filegen** (*st_gen*): file generation number +* **metadata.creation** (*st_birthtime*): time of file creation + +On Mac OS: + +* **metadata.rsize** (*st_rsize*): ? +* **metadata.creator** (*st_creator*): ? +* **metadata.type** (*st_type*): ? + +On RISCOS: + +* **metadata.filetype** (*st_ftype*): file type +* **metadata.attributes** (*st_attrs*): attributes +* **metadata.objecttype** (*st_objtype*): object type + +You may access any of these attributes as either normal attributes, or as dictionary keys. The following are both valid: + +```python +filesize = metadata.size +filesize = metadata['size'] +``` + +Optionally, you may pass `True` as a parameter to either `stat` or `symlink_stat`, to return the original data returned by `os.stat`, without changing the attribute names. This does, however, mean that dictionary key access no longer works. Example: + +```python +metadata = some_file.stat(True) +filesize = metadata.st_size # Valid +filesize = metadata['st_size'] # Won't work! +``` + +### Directory objects + +There are some methods that are specific to directories, and only available on `Directory` objects. + +#### Retrieving a child file/directory + +You can use `get` to retrieve a `File` or `Directory` object for a child node. The type of node will automatically be detected, and either a `File` or `Directory` object will be returned as appropriate. The child doesn't have to be a direct child; it will simply join together the paths, so you can even retrieve nodes outside the path of the current `Directory`. A `FilesystemException` will be raised if the path does not exist. + +Examples: + +```python +child_dir = some_dir.get("assets") +deeper_child_file = some_dir.get("public/static/logo.png") +outside_dir = some_dir.get("../configuration") +``` + +#### Listing all child nodes + +You may retrieve a list of `File` and `Directory` objects representing child nodes of the directory, by using `get_children`. + +```python +child_nodes = some_dir.get_children() +``` + +Alternatively, you may use `get_files` or `get_directories` to only retrieve child files and directories, respectively. All files and directories will be wrapped in `File` and `Directory` objects. + +### File objects + +You may use `File` objects as actual Python file objects. There are three ways to do this: + +#### As a context manager in read-only mode + +The easiest way. The file object will be opened in `rb` (binary reading) mode. It will be automatically closed. + +```python +some_file = filething.File("/some/file/on/my/system") + +with some_file as f: + print f.read() +``` + +#### As a context manager in another mode + +If you need to do more than just reading, you may define an explicit mode. The file will still be automatically closed. + +```python +some_file = filething.File("/some/file/on/my/system") + +with some_file("wb") as f: + f.write("hi!") +``` + +#### As a normal function + +If context managers are not an option for some reason, you may retrieve the corresponding Python file object through a regular method. If you don't specify a mode, it will default to `rb`. + +Note that when using this method, you need to manually close the file! + +```python +some_file = filething.File("/some/file/on/my/system") + +f = some_file.get_file_object("wb") +f.write("hi!") +f.close() +``` \ No newline at end of file diff --git a/filething/__init__.py b/filething/__init__.py new file mode 100644 index 0000000..3b8d01c --- /dev/null +++ b/filething/__init__.py @@ -0,0 +1,131 @@ +import sys, os, collections, contextlib + +def map_attributes(obj, attr_map): + attrs = {} + + for original, mapped in attr_map.iteritems(): + try: + attrs[mapped] = getattr(obj, original) + except AttributeError, e: + pass + + if len(attrs) == 0: + raise FilesystemException("No stat data received! This is probably a bug, please report it.") + + return Attributes(attrs) + +class FilesystemException(Exception): + pass + +class Attributes(object): + def __init__(self, data = {}): + self.data = data + self.__setattr__ = self._setattr # To prevent the previous line from causing havoc + + def __getattr__(self, attr): + try: + return self.data[attr] + except KeyError, e: + raise AttributeError("No such attribute.") + + def _setattr(self, attr, value): + self.data[attr] = value + + def __getitem__(self, attr): + return self.data[attr] + + def __setitem__(self, attr, value): + self.data[attr] = value + +class FilesystemObject(object): + def __init__(self, path): + if not os.path.exists(path): + raise FilesystemException("The specified path (%s) either does not exist, or you cannot access it." % path) + + self.path = path + self.name = os.path.basename(path) + self.is_symlink = os.path.islink(self.path) + + def _process_stat(self, data, original_names): + attr_map = { + "st_mode": "mode", + "st_ino": "inode", + "st_dev": "device", + "st_nlink": "links", + "st_uid": "uid", + "st_gid": "gid", + "st_size": "size", + "st_atime": "lastaccessed", + "st_mtime": "lastmodified", + "st_ctime": "ctime", + "st_blocks": "blockcount", + "st_blksize": "blocksize", + "st_rdev": "devicetype", + "st_flags": "userflags", + "st_gen": "filegen", + "st_birthtime": "creation", + "st_rsize": "rsize", + "st_creator": "creator", + "st_type": "type", + "st_ftype": "filetype", + "st_attrs": "attributes", + "st_objtype": "objecttype" + } + + if original_names: + return data + else: + return map_attributes(data, attr_map) + + def stat(self, original_names = False): + return self._process_stat(os.stat(self.path), original_names) + + def symlink_stat(self, original_names = False): + if self.is_symlink: + return self._process_stat(os.lstat(self.path), original_names) + else: + raise FilesystemException("The specified path is not a symlink.") + +class Directory(FilesystemObject): + def _get_items(self): + return os.listdir(self.path) + + def _join(self, name): + return os.path.join(self.path, name) + + def get(self, name): + child = self._join(name) + + if os.path.isdir(child): + return Directory(child) + else: + return File(child) + + def get_children(self): + return [self.get(x) for x in self._get_items()] + + def get_directories(self): + return [Directory(self._join(x)) for x in self._get_items() if os.path.isdir(self._join(x))] + + def get_files(self): + return [File(self._join(x)) for x in self._get_items() if os.path.isfile(self._join(x))] + +class File(FilesystemObject): + def __enter__(self, mode = "rb"): + self.fileobj = self.get_file_object(mode) + return self.fileobj + + def __exit__(self, exc_type, exc_value, traceback): + self.fileobj.close() + + @contextlib.contextmanager + def __call__(self, mode = "rb"): + obj = self.__enter__(mode) + + try: + yield obj + finally: + self.fileobj.close() # Can't call __exit__ here because we don't have an exception... + + def get_file_object(self, mode = "rb"): + return open(self.path, mode) \ No newline at end of file