You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@buildstream.apache.org by tv...@apache.org on 2021/02/04 07:58:43 UTC
[buildstream] 07/26: Add utils for file timestamp support
This is an automated email from the ASF dual-hosted git repository.
tvb pushed a commit to branch traveltissues/mr4
in repository https://gitbox.apache.org/repos/asf/buildstream.git
commit f735652bb7a3612d0b63b53bb14b62ee6bec4b0e
Author: Darius Makovsky <tr...@protonmail.com>
AuthorDate: Wed Jan 8 16:30:47 2020 +0000
Add utils for file timestamp support
---
requirements/cov-requirements.txt | 14 +++----
requirements/dev-requirements.txt | 18 ++++-----
requirements/requirements.in | 1 +
requirements/requirements.txt | 7 ++--
src/buildstream/utils.py | 77 ++++++++++++++++++++++++++++++++++++
tests/internals/utils_move_atomic.py | 23 ++++++++++-
6 files changed, 120 insertions(+), 20 deletions(-)
diff --git a/requirements/cov-requirements.txt b/requirements/cov-requirements.txt
index c700aba..abf4936 100644
--- a/requirements/cov-requirements.txt
+++ b/requirements/cov-requirements.txt
@@ -3,13 +3,13 @@ pytest-cov==2.8.1
Cython==0.29.14
## The following requirements were added by pip freeze:
attrs==19.3.0
-importlib-metadata==1.1.0
-more-itertools==8.0.0
-packaging==19.2
+importlib-metadata==1.3.0
+more-itertools==8.0.2
+packaging==20.0
pluggy==0.13.1
-py==1.8.0
-pyparsing==2.4.5
-pytest==5.3.1
+py==1.8.1
+pyparsing==2.4.6
+pytest==5.3.2
six==1.13.0
-wcwidth==0.1.7
+wcwidth==0.1.8
zipp==0.6.0
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 15e9b30..f188280 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -1,29 +1,29 @@
pexpect==4.7.0
pylint==2.4.4
-pytest==5.3.1
+pytest==5.3.2
pytest-datafiles==2.0
pytest-env==0.6.2
-pytest-xdist==1.30.0
-pytest-timeout==1.3.3
+pytest-xdist==1.31.0
+pytest-timeout==1.3.4
pyftpdlib==1.5.5
## The following requirements were added by pip freeze:
apipkg==1.5
astroid==2.3.3
attrs==19.3.0
execnet==1.7.1
-importlib-metadata==1.1.0
+importlib-metadata==1.3.0
isort==4.3.21
lazy-object-proxy==1.4.3
mccabe==0.6.1
-more-itertools==8.0.0
-packaging==19.2
+more-itertools==8.0.2
+packaging==20.0
pluggy==0.13.1
ptyprocess==0.6.0
-py==1.8.0
-pyparsing==2.4.5
+py==1.8.1
+pyparsing==2.4.6
pytest-forked==1.1.3
six==1.13.0
typed-ast==1.4.0
-wcwidth==0.1.7
+wcwidth==0.1.8
wrapt==1.11.2
zipp==0.6.0
diff --git a/requirements/requirements.in b/requirements/requirements.in
index 50bb523..a595f69 100644
--- a/requirements/requirements.in
+++ b/requirements/requirements.in
@@ -9,3 +9,4 @@ ruamel.yaml.clib >= 0.1.2
setuptools
pyroaring
ujson
+python-dateutil>= 2.7.0
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 9620908..1bc75f2 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -1,14 +1,15 @@
Click==7.0
-grpcio==1.25.0
+grpcio==1.26.0
Jinja2==2.10.3
pluginbase==1.0.0
-protobuf==3.11.0
+protobuf==3.11.2
psutil==5.6.7
ruamel.yaml==0.16.5
+ruamel.yaml.clib==0.2.0
setuptools==39.0.1
pyroaring==0.2.9
ujson==1.35
+python-dateutil==2.8.1
## The following requirements were added by pip freeze:
MarkupSafe==1.1.1
-ruamel.yaml.clib==0.2.0
six==1.13.0
diff --git a/src/buildstream/utils.py b/src/buildstream/utils.py
index 545816e..c2a43d9 100644
--- a/src/buildstream/utils.py
+++ b/src/buildstream/utils.py
@@ -33,10 +33,12 @@ from stat import S_ISDIR
import subprocess
import tempfile
import time
+import datetime
import itertools
from contextlib import contextmanager
from pathlib import Path
from typing import Callable, IO, Iterable, Iterator, Optional, Tuple, Union
+from dateutil import parser as dateutil_parser
import psutil
@@ -132,6 +134,81 @@ class FileListResult:
return ret
+def _make_timestamp(timepoint: float) -> str:
+ """Obtain the ISO 8601 timestamp represented by the time given in seconds.
+
+ Args:
+ timepoint (float): the time since the epoch in seconds
+
+ Returns:
+ (str): the timestamp specified by https://www.ietf.org/rfc/rfc3339.txt
+ with a UTC timezone code 'Z'.
+
+ """
+ assert isinstance(timepoint, float), "Time to render as timestamp must be a float: {}".format(str(timepoint))
+ try:
+ return datetime.datetime.utcfromtimestamp(timepoint).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+ except (OverflowError, TypeError):
+ raise UtilError("Failed to make UTC timestamp from {}".format(timepoint))
+
+
+def _get_file_mtimestamp(fullpath: str) -> str:
+ """Obtain the ISO 8601 timestamp represented by the mtime of the
+ file at the given path."""
+ assert isinstance(fullpath, str), "Path to file must be a string: {}".format(str(fullpath))
+ try:
+ mtime = os.path.getmtime(fullpath)
+ except OSError:
+ raise UtilError("Failed to get mtime of file at {}".format(fullpath))
+ return _make_timestamp(mtime)
+
+
+def _parse_timestamp(timestamp: str) -> float:
+ """Parse an ISO 8601 timestamp as specified in
+ https://www.ietf.org/rfc/rfc3339.txt. Only timestamps with the UTC code
+ 'Z' or an offset are valid. For example: '2019-12-12T10:23:01.54Z' or
+ '2019-12-12T10:23:01.54+00:00'.
+
+ Args:
+ timestamp (str): the timestamp
+
+ Returns:
+ (float): The time in seconds since epoch represented by the
+ timestamp.
+
+ Raises:
+ UtilError: if extraction of seconds fails
+ """
+ assert isinstance(timestamp, str), "Timestamp to parse must be a string: {}".format(str(timestamp))
+ try:
+ errmsg = "Failed to parse given timestamp: " + timestamp
+ parsed_time = dateutil_parser.isoparse(timestamp)
+ if parsed_time.tzinfo:
+ return parsed_time.timestamp()
+ raise UtilError(errmsg)
+ except (ValueError, OverflowError, TypeError):
+ raise UtilError(errmsg)
+
+
+def _set_file_mtime(fullpath: str, seconds: Union[int, float]) -> None:
+ """Set the access and modification times of the file at the given path
+ to the given time. The time of the file will be set with nanosecond
+ resolution if supported.
+
+ Args:
+ fullpath (str): the string representing the path to the file
+ timestamp (int, float): the time in seconds since the UNIX epoch
+ """
+ assert isinstance(fullpath, str), "Path to file must be a string: {}".format(str(fullpath))
+ assert isinstance(seconds, (int, float)), "Mtime to set must be a float or integer: {}".format(str(seconds))
+ set_mtime = seconds * 10 ** 9
+ try:
+ os.utime(fullpath, times=None, ns=(int(set_mtime), int(set_mtime)))
+ except OSError:
+ errmsg = "Failed to set the times of the file at {} to {}".format(fullpath, str(seconds))
+ raise UtilError(errmsg)
+
+
def list_relative_paths(directory: str) -> Iterator[str]:
"""A generator for walking directory relative paths
diff --git a/tests/internals/utils_move_atomic.py b/tests/internals/utils_move_atomic.py
index cda0208..dd417cb 100644
--- a/tests/internals/utils_move_atomic.py
+++ b/tests/internals/utils_move_atomic.py
@@ -3,7 +3,13 @@
import pytest
-from buildstream.utils import move_atomic, DirectoryExistsError
+from buildstream.utils import (
+ move_atomic,
+ DirectoryExistsError,
+ _get_file_mtimestamp,
+ _set_file_mtime,
+ _parse_timestamp,
+)
@pytest.fixture
@@ -89,3 +95,18 @@ def test_move_to_existing_non_empty_dir(src, tmp_path):
with pytest.raises(DirectoryExistsError):
move_atomic(src, dst)
+
+
+def test_move_to_empty_dir_set_mtime(src, tmp_path):
+ dst = tmp_path.joinpath("dst")
+ move_atomic(src, dst)
+ assert dst.joinpath("test").exists()
+ _dst = str(dst)
+ # set the mtime via stamp
+ timestamp1 = "2020-01-08T11:05:50.832123Z"
+ _set_file_mtime(_dst, _parse_timestamp(timestamp1))
+ assert timestamp1 == _get_file_mtimestamp(_dst)
+ # reset the mtime using an offset stamp
+ timestamp2 = "2010-02-12T12:05:50.832123+01:00"
+ _set_file_mtime(_dst, _parse_timestamp(timestamp2))
+ assert _get_file_mtimestamp(_dst) == "2010-02-12T11:05:50.832123Z"