Skip to content

Commit e4fbe90

Browse files
authored
Symlinks (#64)
* symlink info * test fix for Py3 * made certain namespaces required in info attributes * target fix * without scandir fix * landscape fix * docs * info tests * version bump * added permissiondenied error * unused vars * test info.is_link
1 parent d5713fc commit e4fbe90

16 files changed

Lines changed: 277 additions & 21 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## Unreleased
8+
9+
### Added
10+
- Lstat info namespace
11+
- Link info namespace
12+
- FS.islink method
13+
- Info.is_link method
14+
15+
716
## [2.0.7] - 2017-08-06
817

918
### Fixes

docs/source/info.rst

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ Name type Description
109109
gid int The group ID.
110110
group str The group name.
111111
permissions Permissions An instance of
112-
:class:`~fs.permissions.Permissions`, which
113-
contains the permissions for the resource.
112+
:class:`~fs.permissions.Permissions`,
113+
which contains the permissions for the
114+
resource.
114115
uid int The user ID.
115116
user str The user name of the owner.
116117
================ =================== ==========================================
@@ -129,6 +130,30 @@ filesystems which map directly to the OS filesystem. Most other
129130
filesystems will not support this namespace.
130131

131132

133+
LStat Namespace
134+
~~~~~~~~~~~~~~~
135+
136+
The ``lstat`` namespace contains information reported by a call to
137+
`os.lstat <https://docs.python.org/3.5/library/stat.html>`_. This
138+
namespace is supported by :class:`~fs.osfs.OSFS` and potentially other
139+
filesystems which map directly to the OS filesystem. Most other
140+
filesystems will not support this namespace.
141+
142+
Link Namespace
143+
~~~~~~~~~~~~~~
144+
145+
The ``link`` namespace contains information about a symlink.
146+
147+
=================== ======= ============================================
148+
Name type Description
149+
------------------- ------- --------------------------------------------
150+
target str A path to the symlink target, or ``None`` if
151+
this path is not a symlink.
152+
Note, the meaning of this target is somewhat
153+
filesystem dependent, and may not be a valid
154+
path on the FS object.
155+
=================== ======= ============================================
156+
132157
Other Namespaces
133158
~~~~~~~~~~~~~~~~
134159

@@ -145,6 +170,30 @@ with the :meth:`~fs.info.Info.get` method.
145170
filesystem does *not* support. Any unknown namespaces will be
146171
ignored.
147172

173+
Missing Namespaces
174+
------------------
175+
176+
Some attributes on the Info objects require that a given namespace be
177+
present. If you attempt to reference them without the namespace being
178+
present (because you didn't request it, or the filesystem doesn't
179+
support it) then a :class:`~fs.errors.MissingInfoNamespace` exception
180+
will be thrown. Here's how you might handle such exceptions::
181+
182+
try:
183+
print('user is {}'.format(info.user))
184+
except errors.MissingInfoNamespace:
185+
# No 'access' namespace
186+
pass
187+
188+
If you prefer a *look before you leap* approach, you can use use the
189+
:meth:`~fs.info.Info.has_namespace` method. Here's an example::
190+
191+
192+
if info.has_namespace('access'):
193+
print('user is {}'.format(info.user))
194+
195+
See :class:`~fs.info.Info` for details regarding info attributes.
196+
148197
Raw Info
149198
--------
150199

fs/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.7"
1+
__version__ = "2.0.8a1"

fs/base.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ def makedir(self, path, permissions=None, recreate=False):
117117
:param permissions: :class:`~fs.permissions.Permissions`
118118
instance.
119119
:type permissions: Permissions
120-
:param bool recreate: Do not raise an error if the directory exists.
120+
:param bool recreate: Do not raise an error if the directory
121+
exists.
121122
:rtype: :class:`~fs.subfs.SubFS`
122123
123124
:raises fs.errors.DirectoryExists: if the path already exists.
@@ -707,6 +708,17 @@ def isfile(self, path):
707708
except errors.ResourceNotFound:
708709
return False
709710

711+
def islink(self, path):
712+
"""
713+
Check if a path is a symlink.
714+
715+
:param str path: A path on the filesystem.
716+
:rtype: bool
717+
718+
"""
719+
self.getinfo(path)
720+
return False
721+
710722
def lock(self):
711723
"""
712724
Get a context manager that *locks* the filesystem.
@@ -727,7 +739,7 @@ def lock(self):
727739
multiple filesystem methods. Individual methods are thread safe
728740
already, and don't need to be locked.
729741
730-
..note ::
742+
.. note::
731743
This only locks at the Python level. There is nothing to
732744
prevent other processes from modifying the filesystem
733745
outside of the filesystem instance.

fs/compress.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,11 @@ def write_tar(src_fs,
171171
tar_info.mtime = mtime
172172

173173
for tarattr, infoattr in tar_attr:
174-
if getattr(info, infoattr) is not None:
175-
setattr(tar_info, tarattr, getattr(info, infoattr))
174+
if getattr(info, infoattr, None) is not None:
175+
setattr(tar_info, tarattr, getattr(info, infoattr, None))
176176

177-
tar_info.mode = getattr(info.permissions, 'mode', 0o420)
177+
if info.has_namespace('access'):
178+
tar_info.mode = getattr(info.permissions, 'mode', 0o420)
178179

179180
if info.is_dir:
180181
tar_info.type = tarfile.DIRTYPE

fs/error_tools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class _ConvertOSErrors(object):
1919

2020
FILE_ERRORS = {
2121
64: errors.RemoteConnectionError, # ENONET
22+
errno.EACCES: errors.PermissionDenied,
2223
errno.ENOENT: errors.ResourceNotFound,
2324
errno.EFAULT: errors.ResourceNotFound,
2425
errno.ESRCH: errors.ResourceNotFound,

fs/errors.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
'InsufficientStorage',
3131
'InvalidCharsInPath',
3232
'InvalidPath',
33+
'MissingInfoNamespace',
3334
'NoURL',
3435
'OperationFailed',
3536
'OperationTimeout',
@@ -45,6 +46,16 @@
4546
]
4647

4748

49+
class MissingInfoNamespace(AttributeError):
50+
"""Raised when an expected namespace was missing."""
51+
52+
def __init__(self, namespace):
53+
msg = "namespace '{}' is required for this attribute"
54+
super(MissingInfoNamespace, self).__init__(
55+
msg.format(namespace)
56+
)
57+
58+
4859
@six.python_2_unicode_compatible
4960
class FSError(Exception):
5061
"""Base exception class for the FS module."""

fs/info.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
from copy import deepcopy
66

7-
from . import filesize
87
from .path import join
98
from .enums import ResourceType
9+
from .errors import MissingInfoNamespace
1010
from .permissions import Permissions
1111
from .time import epoch_to_datetime
1212

@@ -59,10 +59,8 @@ def get(self, namespace, key, default=None):
5959
>>> info.get('access', 'permissions')
6060
['u_r', 'u_w', '_wx']
6161
62-
:param namespace: A namespace identifier.
63-
:type namespace: str
64-
:param key: A key within the namespace.
65-
:type key: str
62+
:param str namespace: A namespace identifier.
63+
:param str key: A key within the namespace.
6664
:param default: A default value to return if either the
6765
namespace or namespace + key is not found.
6866
"""
@@ -71,6 +69,15 @@ def get(self, namespace, key, default=None):
7169
except KeyError:
7270
return default
7371

72+
def _require_namespace(self, namespace):
73+
"""
74+
Raise a MissingInfoNamespace if the given namespace is not
75+
present in the info.
76+
77+
"""
78+
if namespace not in self.raw:
79+
raise MissingInfoNamespace(namespace)
80+
7481
def is_writeable(self, namespace, key):
7582
"""
7683
Check if a given key in a namespace is writable (with
@@ -146,6 +153,17 @@ def is_file(self):
146153
"""
147154
return not self.get('basic', 'is_dir')
148155

156+
@property
157+
def is_link(self):
158+
"""
159+
Check if a resource is a symlink.
160+
161+
:rtype: bool
162+
163+
"""
164+
self._require_namespace('link')
165+
return self.get('link', 'target') is not None
166+
149167
@property
150168
def type(self):
151169
"""
@@ -154,8 +172,11 @@ def type(self):
154172
Requires the ``"details"`` namespace.
155173
156174
:type: :class:`~fs.ResourceType`
175+
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
176+
namespace is not in the Info.
157177
158178
"""
179+
self._require_namespace('details')
159180
return ResourceType(self.get('details', 'type', 0))
160181

161182
@property
@@ -167,8 +188,11 @@ def accessed(self):
167188
Requires the ``"details"`` namespace.
168189
169190
:rtype: datetime
191+
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
192+
namespace is not in the Info.
170193
171194
"""
195+
self._require_namespace('details')
172196
_time = self._make_datetime(
173197
self.get('details', 'accessed')
174198
)
@@ -183,8 +207,11 @@ def modified(self):
183207
Requires the ``"details"`` namespace.
184208
185209
:rtype: datetime
210+
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
211+
namespace is not in the Info.
186212
187213
"""
214+
self._require_namespace('details')
188215
_time = self._make_datetime(
189216
self.get('details', 'modified')
190217
)
@@ -199,8 +226,11 @@ def created(self):
199226
Requires the ``"details"`` namespace.
200227
201228
:rtype: datetime
229+
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
230+
namespace is not in the Info.
202231
203232
"""
233+
self._require_namespace('details')
204234
_time = self._make_datetime(
205235
self.get('details', 'created')
206236
)
@@ -215,8 +245,11 @@ def metadata_changed(self):
215245
Requires the ``"details"`` namespace.
216246
217247
:rtype: datetime
248+
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
249+
namespace is not in the Info.
218250
219251
"""
252+
self._require_namespace('details')
220253
_time = self._make_datetime(
221254
self.get('details', 'metadata_changed')
222255
)
@@ -230,8 +263,11 @@ def permissions(self):
230263
Requires the ``"access"`` namespace.
231264
232265
:rtype: :class:`fspermissions.Permissions`
266+
:raises ~fs.errors.MissingInfoNamespace: if the 'ACCESS'
267+
namespace is not in the Info.
233268
234269
"""
270+
self._require_namespace('access')
235271
_perm_names = self.get('access', 'permissions')
236272
if _perm_names is None:
237273
return None
@@ -246,8 +282,11 @@ def size(self):
246282
Requires the ``"details"`` namespace.
247283
248284
:rtype: int
285+
:raises ~fs.errors.MissingInfoNamespace: if the 'details'
286+
namespace is not in the Info.
249287
250288
"""
289+
self._require_namespace('details')
251290
return self.get('details', 'size')
252291

253292
@property
@@ -258,8 +297,11 @@ def user(self):
258297
Requires the ``"access"`` namespace.
259298
260299
:rtype: str
300+
:raises ~fs.errors.MissingInfoNamespace: if the 'access'
301+
namespace is not in the Info.
261302
262303
"""
304+
self._require_namespace('access')
263305
return self.get('access', 'user')
264306

265307
@property
@@ -270,8 +312,11 @@ def uid(self):
270312
Requires the ``"access"`` namespace.
271313
272314
:rtype: int
315+
:raises ~fs.errors.MissingInfoNamespace: if the 'access'
316+
namespace is not in the Info.
273317
274318
"""
319+
self._require_namespace('access')
275320
return self.get('access', 'uid')
276321

277322
@property
@@ -283,8 +328,11 @@ def group(self):
283328
Requires the ``"access"`` namespace.
284329
285330
:rtype: str
331+
:raises ~fs.errors.MissingInfoNamespace: if the 'access'
332+
namespace is not in the Info.
286333
287334
"""
335+
self._require_namespace('access')
288336
return self.get('access', 'group')
289337

290338
@property
@@ -295,6 +343,25 @@ def gid(self):
295343
Requires the ``"access"`` namespace.
296344
297345
:rtype: int
346+
:raises ~fs.errors.MissingInfoNamespace: if the 'access'
347+
namespace is not in the Info.
298348
299349
"""
350+
self._require_namespace('access')
300351
return self.get('access', 'gid')
352+
353+
@property
354+
def target(self):
355+
"""
356+
Get the link target, if this is a symlink, or ``None`` if this
357+
is not a symlink.
358+
359+
Requires the ``"link"`` namespace.
360+
361+
:rtype: bool
362+
:raises ~fs.errors.MissingInfoNamespace: if the 'link'
363+
namespace is not in the Info.
364+
365+
"""
366+
self._require_namespace('link')
367+
return self.get('link', 'target')

fs/iotools.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from __future__ import print_function
22
from __future__ import unicode_literals
33

4-
import six
5-
64
import io
75
from io import SEEK_SET, SEEK_CUR
86

fs/memoryfs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def size(self):
178178

179179
def get_entry(self, name, default=None):
180180
assert self.is_dir, 'must be a directory'
181-
return self._dir.get(name)
181+
return self._dir.get(name, default)
182182

183183
def set_entry(self, name, dir_entry):
184184
self._dir[name] = dir_entry

0 commit comments

Comments
 (0)