Skip to content

Commit

Permalink
Merge 2ab753a into d5713fc
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Aug 12, 2017
2 parents d5713fc + 2ab753a commit 571dbee
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 14 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added
- Lstat info namespace
- Link info namespace
- islink method


## [2.0.7] - 2017-08-06

### Fixes
Expand Down
24 changes: 24 additions & 0 deletions docs/source/info.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,29 @@ filesystems which map directly to the OS filesystem. Most other
filesystems will not support this namespace.


LStat Namespace
~~~~~~~~~~~~~~~

The ``lstat`` namespace contains information reported by a call to
`os.lstat <https://docs.python.org/3.5/library/stat.html>`_. This
namespace is supported by :class:`~fs.osfs.OSFS` and potentially other
filesystems which map directly to the OS filesystem. Most other
filesystems will not support this namespace.

Link Namespace
~~~~~~~~~~~~~~

The ``link`` namespace contains information about a symlink.

=================== ======= ============================================
Name type Description
------------------- ------- --------------------------------------------
target str A path to the symlink target, or ``None`` if
this path is not a symlink. Note this path
is a *system* path, and not a path on the
filesystem object.
=================== ======= ============================================

Other Namespaces
~~~~~~~~~~~~~~~~

Expand All @@ -145,6 +168,7 @@ with the :meth:`~fs.info.Info.get` method.
filesystem does *not* support. Any unknown namespaces will be
ignored.


Raw Info
--------

Expand Down
2 changes: 1 addition & 1 deletion fs/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.7"
__version__ = "2.0.8a0"
16 changes: 14 additions & 2 deletions fs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ def makedir(self, path, permissions=None, recreate=False):
:param permissions: :class:`~fs.permissions.Permissions`
instance.
:type permissions: Permissions
:param bool recreate: Do not raise an error if the directory exists.
:param bool recreate: Do not raise an error if the directory
exists.
:rtype: :class:`~fs.subfs.SubFS`
:raises fs.errors.DirectoryExists: if the path already exists.
Expand Down Expand Up @@ -707,6 +708,17 @@ def isfile(self, path):
except errors.ResourceNotFound:
return False

def islink(self, path):
"""
Check if a path is a symlink.
:param str path: A path on the filesystem.
:rtype: bool
"""
self.getinfo(path)
return False

def lock(self):
"""
Get a context manager that *locks* the filesystem.
Expand All @@ -727,7 +739,7 @@ def lock(self):
multiple filesystem methods. Individual methods are thread safe
already, and don't need to be locked.
..note ::
.. note::
This only locks at the Python level. There is nothing to
prevent other processes from modifying the filesystem
outside of the filesystem instance.
Expand Down
7 changes: 4 additions & 3 deletions fs/compress.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,11 @@ def write_tar(src_fs,
tar_info.mtime = mtime

for tarattr, infoattr in tar_attr:
if getattr(info, infoattr) is not None:
setattr(tar_info, tarattr, getattr(info, infoattr))
if getattr(info, infoattr, None) is not None:
setattr(tar_info, tarattr, getattr(info, infoattr, None))

tar_info.mode = getattr(info.permissions, 'mode', 0o420)
if info.has_namespace('access'):
tar_info.mode = getattr(info.permissions, 'mode', 0o420)

if info.is_dir:
tar_info.type = tarfile.DIRTYPE
Expand Down
11 changes: 11 additions & 0 deletions fs/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'InsufficientStorage',
'InvalidCharsInPath',
'InvalidPath',
'MissingInfoNamespace',
'NoURL',
'OperationFailed',
'OperationTimeout',
Expand All @@ -45,6 +46,16 @@
]


class MissingInfoNamespace(AttributeError):
"""Raised when an expected namespace was missing."""

def __init__(self, namespace):
msg = "namespace '{}' is required for this attribute"
super(MissingInfoNamespace, self).__init__(
msg.format(namespace)
)


@six.python_2_unicode_compatible
class FSError(Exception):
"""Base exception class for the FS module."""
Expand Down
77 changes: 72 additions & 5 deletions fs/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from copy import deepcopy

from . import filesize
from .path import join
from .enums import ResourceType
from .errors import MissingInfoNamespace
from .permissions import Permissions
from .time import epoch_to_datetime

Expand Down Expand Up @@ -59,10 +59,8 @@ def get(self, namespace, key, default=None):
>>> info.get('access', 'permissions')
['u_r', 'u_w', '_wx']
:param namespace: A namespace identifier.
:type namespace: str
:param key: A key within the namespace.
:type key: str
:param str namespace: A namespace identifier.
:param str key: A key within the namespace.
:param default: A default value to return if either the
namespace or namespace + key is not found.
"""
Expand All @@ -71,6 +69,15 @@ def get(self, namespace, key, default=None):
except KeyError:
return default

def _require_namespace(self, namespace):
"""
Raise a MissingInfoNamespace if the given namespace is not
present in the info.
"""
if namespace not in self.raw:
raise MissingInfoNamespace(namespace)

def is_writeable(self, namespace, key):
"""
Check if a given key in a namespace is writable (with
Expand Down Expand Up @@ -146,6 +153,17 @@ def is_file(self):
"""
return not self.get('basic', 'is_dir')

@property
def is_link(self):
"""
Check if a resource is a symlink.
:rtype: bool
"""
self._require_namespace('link')
return self.get('link', 'target') is not None

@property
def type(self):
"""
Expand All @@ -154,8 +172,11 @@ def type(self):
Requires the ``"details"`` namespace.
:type: :class:`~fs.ResourceType`
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
namespace is not in the Info.
"""
self._require_namespace('details')
return ResourceType(self.get('details', 'type', 0))

@property
Expand All @@ -167,8 +188,11 @@ def accessed(self):
Requires the ``"details"`` namespace.
:rtype: datetime
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
namespace is not in the Info.
"""
self._require_namespace('details')
_time = self._make_datetime(
self.get('details', 'accessed')
)
Expand All @@ -183,8 +207,11 @@ def modified(self):
Requires the ``"details"`` namespace.
:rtype: datetime
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
namespace is not in the Info.
"""
self._require_namespace('details')
_time = self._make_datetime(
self.get('details', 'modified')
)
Expand All @@ -199,8 +226,11 @@ def created(self):
Requires the ``"details"`` namespace.
:rtype: datetime
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
namespace is not in the Info.
"""
self._require_namespace('details')
_time = self._make_datetime(
self.get('details', 'created')
)
Expand All @@ -215,8 +245,11 @@ def metadata_changed(self):
Requires the ``"details"`` namespace.
:rtype: datetime
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
namespace is not in the Info.
"""
self._require_namespace('details')
_time = self._make_datetime(
self.get('details', 'metadata_changed')
)
Expand All @@ -230,8 +263,11 @@ def permissions(self):
Requires the ``"access"`` namespace.
:rtype: :class:`fspermissions.Permissions`
:raises ~fs.errors.MissingInfoNamespace: if the 'ACCESS'
namespace is not in the Info.
"""
self._require_namespace('access')
_perm_names = self.get('access', 'permissions')
if _perm_names is None:
return None
Expand All @@ -246,8 +282,11 @@ def size(self):
Requires the ``"details"`` namespace.
:rtype: int
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
namespace is not in the Info.
"""
self._require_namespace('details')
return self.get('details', 'size')

@property
Expand All @@ -258,8 +297,11 @@ def user(self):
Requires the ``"access"`` namespace.
:rtype: str
:raises ~fs.errors.MissingInfoNamespace: if the 'access'
namespace is not in the Info.
"""
self._require_namespace('access')
return self.get('access', 'user')

@property
Expand All @@ -270,8 +312,11 @@ def uid(self):
Requires the ``"access"`` namespace.
:rtype: int
:raises ~fs.errors.MissingInfoNamespace: if the 'access'
namespace is not in the Info.
"""
self._require_namespace('access')
return self.get('access', 'uid')

@property
Expand All @@ -283,8 +328,11 @@ def group(self):
Requires the ``"access"`` namespace.
:rtype: str
:raises ~fs.errors.MissingInfoNamespace: if the 'access'
namespace is not in the Info.
"""
self._require_namespace('access')
return self.get('access', 'group')

@property
Expand All @@ -295,6 +343,25 @@ def gid(self):
Requires the ``"access"`` namespace.
:rtype: int
:raises ~fs.errors.MissingInfoNamespace: if the 'access'
namespace is not in the Info.
"""
self._require_namespace('access')
return self.get('access', 'gid')

@property
def target(self):
"""
Get the link target, if this is a symlink, or ``None`` if this
is not a symlink.
Requires the ``"link"`` namespace.
:rtype: bool
:raises ~fs.errors.MissingInfoNamespace: if the 'link'
namespace is not in the Info.
"""
self._require_namespace('link')
return self.get('link', 'target')
Loading

0 comments on commit 571dbee

Please sign in to comment.