1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
|
"""Container for filesystem resource informations.
"""
from __future__ import absolute_import, print_function, unicode_literals
import typing
from typing import cast
from copy import deepcopy
from ._typing import Text, overload
from .enums import ResourceType
from .errors import MissingInfoNamespace
from .path import join
from .permissions import Permissions
from .time import epoch_to_datetime
if typing.TYPE_CHECKING:
from typing import Any, Callable, List, Mapping, Optional, Union
from datetime import datetime
RawInfo = Mapping[Text, Mapping[Text, object]]
ToDatetime = Callable[[int], datetime]
T = typing.TypeVar("T")
class Info(object):
"""Container for :ref:`info`.
Resource information is returned by the following methods:
* `~fs.base.FS.getinfo`
* `~fs.base.FS.scandir`
* `~fs.base.FS.filterdir`
Arguments:
raw_info (dict): A dict containing resource info.
to_datetime (callable): A callable that converts an
epoch time to a datetime object. The default uses
`~fs.time.epoch_to_datetime`.
"""
__slots__ = ["raw", "_to_datetime", "namespaces"]
def __init__(self, raw_info, to_datetime=epoch_to_datetime):
# type: (RawInfo, ToDatetime) -> None
"""Create a resource info object from a raw info dict."""
self.raw = raw_info
self._to_datetime = to_datetime
self.namespaces = frozenset(self.raw.keys())
def __str__(self):
# type: () -> str
if self.is_dir:
return "<dir '{}'>".format(self.name)
else:
return "<file '{}'>".format(self.name)
__repr__ = __str__
def __eq__(self, other):
# type: (object) -> bool
return self.raw == getattr(other, "raw", None)
@overload
def _make_datetime(self, t):
# type: (None) -> None
pass
@overload
def _make_datetime(self, t): # noqa: F811
# type: (int) -> datetime
pass
def _make_datetime(self, t): # noqa: F811
# type: (Optional[int]) -> Optional[datetime]
if t is not None:
return self._to_datetime(t)
else:
return None
@overload
def get(self, namespace, key):
# type: (Text, Text) -> Any
pass
@overload # noqa: F811
def get(self, namespace, key, default): # noqa: F811
# type: (Text, Text, T) -> Union[Any, T]
pass
def get(self, namespace, key, default=None): # noqa: F811
# type: (Text, Text, Optional[Any]) -> Optional[Any]
"""Get a raw info value.
Arguments:
namespace (str): A namespace identifier.
key (str): A key within the namespace.
default (object, optional): A default value to return
if either the namespace or the key within the namespace
is not found.
Example:
>>> info = my_fs.getinfo("foo.py", namespaces=["details"])
>>> info.get('details', 'type')
2
"""
try:
return self.raw[namespace].get(key, default) # type: ignore
except KeyError:
return default
def _require_namespace(self, namespace):
# type: (Text) -> None
"""Check if the given namespace is present in the info.
Raises:
~fs.errors.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):
# type: (Text, Text) -> bool
"""Check if a given key in a namespace is writable.
When creating an `Info` object, you can add a ``_write`` key to
each raw namespace that lists which keys are writable or not.
In general, this means they are compatible with the `setinfo`
function of filesystem objects.
Arguments:
namespace (str): A namespace identifier.
key (str): A key within the namespace.
Returns:
bool: `True` if the key can be modified, `False` otherwise.
Example:
Create an `Info` object that marks only the ``modified`` key
as writable in the ``details`` namespace::
>>> now = time.time()
>>> info = Info({
... "basic": {"name": "foo", "is_dir": False},
... "details": {
... "modified": now,
... "created": now,
... "_write": ["modified"],
... }
... })
>>> info.is_writeable("details", "created")
False
>>> info.is_writeable("details", "modified")
True
"""
_writeable = self.get(namespace, "_write", ())
return key in _writeable
def has_namespace(self, namespace):
# type: (Text) -> bool
"""Check if the resource info contains a given namespace.
Arguments:
namespace (str): A namespace identifier.
Returns:
bool: `True` if the namespace was found, `False` otherwise.
"""
return namespace in self.raw
def copy(self, to_datetime=None):
# type: (Optional[ToDatetime]) -> Info
"""Create a copy of this resource info object."""
return Info(deepcopy(self.raw), to_datetime=to_datetime or self._to_datetime)
def make_path(self, dir_path):
# type: (Text) -> Text
"""Make a path by joining ``dir_path`` with the resource name.
Arguments:
dir_path (str): A path to a directory.
Returns:
str: A path to the resource.
"""
return join(dir_path, self.name)
@property
def name(self):
# type: () -> Text
"""`str`: the resource name."""
return cast(Text, self.get("basic", "name"))
@property
def suffix(self):
# type: () -> Text
"""`str`: the last component of the name (with dot).
In case there is no suffix, an empty string is returned.
Example:
>>> info = my_fs.getinfo("foo.py")
>>> info.suffix
'.py'
>>> info2 = my_fs.getinfo("bar")
>>> info2.suffix
''
"""
name = self.get("basic", "name")
if name.startswith(".") and name.count(".") == 1:
return ""
basename, dot, ext = name.rpartition(".")
return "." + ext if dot else ""
@property
def suffixes(self):
# type: () -> List[Text]
"""`List`: a list of any suffixes in the name.
Example:
>>> info = my_fs.getinfo("foo.tar.gz")
>>> info.suffixes
['.tar', '.gz']
"""
name = self.get("basic", "name")
if name.startswith(".") and name.count(".") == 1:
return []
return ["." + suffix for suffix in name.split(".")[1:]]
@property
def stem(self):
# type: () -> Text
"""`str`: the name minus any suffixes.
Example:
>>> info = my_fs.getinfo("foo.tar.gz")
>>> info.stem
'foo'
"""
name = self.get("basic", "name")
if name.startswith("."):
return name
return name.split(".")[0]
@property
def is_dir(self):
# type: () -> bool
"""`bool`: `True` if the resource references a directory."""
return cast(bool, self.get("basic", "is_dir"))
@property
def is_file(self):
# type: () -> bool
"""`bool`: `True` if the resource references a file."""
return not cast(bool, self.get("basic", "is_dir"))
@property
def is_link(self):
# type: () -> bool
"""`bool`: `True` if the resource is a symlink."""
self._require_namespace("link")
return self.get("link", "target", None) is not None
@property
def type(self):
# type: () -> ResourceType
"""`~fs.enums.ResourceType`: the type of the resource.
Requires the ``"details"`` namespace.
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
def accessed(self):
# type: () -> Optional[datetime]
"""`~datetime.datetime`: the resource last access time, or `None`.
Requires the ``"details"`` namespace.
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"))
return _time
@property
def modified(self):
# type: () -> Optional[datetime]
"""`~datetime.datetime`: the resource last modification time, or `None`.
Requires the ``"details"`` namespace.
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"))
return _time
@property
def created(self):
# type: () -> Optional[datetime]
"""`~datetime.datetime`: the resource creation time, or `None`.
Requires the ``"details"`` namespace.
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"))
return _time
@property
def metadata_changed(self):
# type: () -> Optional[datetime]
"""`~datetime.datetime`: the resource metadata change time, or `None`.
Requires the ``"details"`` namespace.
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"))
return _time
@property
def permissions(self):
# type: () -> Optional[Permissions]
"""`Permissions`: the permissions of the resource, or `None`.
Requires the ``"access"`` namespace.
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
permissions = Permissions(_perm_names)
return permissions
@property
def size(self):
# type: () -> int
"""`int`: the size of the resource, in bytes.
Requires the ``"details"`` namespace.
Raises:
~fs.errors.MissingInfoNamespace: if the ``"details"``
namespace is not in the Info.
"""
self._require_namespace("details")
return cast(int, self.get("details", "size"))
@property
def user(self):
# type: () -> Optional[Text]
"""`str`: the owner of the resource, or `None`.
Requires the ``"access"`` namespace.
Raises:
~fs.errors.MissingInfoNamespace: if the ``"access"``
namespace is not in the Info.
"""
self._require_namespace("access")
return self.get("access", "user")
@property
def uid(self):
# type: () -> Optional[int]
"""`int`: the user id of the resource, or `None`.
Requires the ``"access"`` namespace.
Raises:
~fs.errors.MissingInfoNamespace: if the ``"access"``
namespace is not in the Info.
"""
self._require_namespace("access")
return self.get("access", "uid")
@property
def group(self):
# type: () -> Optional[Text]
"""`str`: the group of the resource owner, or `None`.
Requires the ``"access"`` namespace.
Raises:
~fs.errors.MissingInfoNamespace: if the ``"access"``
namespace is not in the Info.
"""
self._require_namespace("access")
return self.get("access", "group")
@property
def gid(self):
# type: () -> Optional[int]
"""`int`: the group id of the resource, or `None`.
Requires the ``"access"`` namespace.
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): # noqa: D402
# type: () -> Optional[Text]
"""`str`: the link target (if resource is a symlink), or `None`.
Requires the ``"link"`` namespace.
Raises:
~fs.errors.MissingInfoNamespace: if the ``"link"``
namespace is not in the Info.
"""
self._require_namespace("link")
return self.get("link", "target")
|