Skip to content

Commit 632b13b

Browse files
authored
Merge pull request #228 from reef-technologies/remove-command-errors
cleanup sync errors related to directories
2 parents 1c0d6ee + 268d1a9 commit 632b13b

13 files changed

+253
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
* Add `get_bucket_name_or_none_from_bucket_id` to `AccountInfo` and `Cache`
1111

1212
### Fixed
13+
* Cleanup sync errors related to directories
1314
* Use proper error handling in `ScanPoliciesManager`
1415

1516
### Changed

b2sdk/_v2/exception.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
from b2sdk.exception import CapExceeded
3131
from b2sdk.exception import ChecksumMismatch
3232
from b2sdk.exception import ClockSkew
33-
from b2sdk.exception import CommandError
3433
from b2sdk.exception import Conflict
3534
from b2sdk.exception import ConnectionReset
3635
from b2sdk.exception import DestFileNewer
@@ -67,9 +66,12 @@
6766
from b2sdk.exception import SSECKeyError
6867
from b2sdk.exception import WrongEncryptionModeForBucketDefault
6968
from b2sdk.exception import interpret_b2_error
69+
from b2sdk.sync.exception import EmptyDirectory
7070
from b2sdk.sync.exception import EnvironmentEncodingError
7171
from b2sdk.sync.exception import IncompleteSync
7272
from b2sdk.sync.exception import InvalidArgument
73+
from b2sdk.sync.exception import NotADirectory
74+
from b2sdk.sync.exception import UnableToCreateDirectory
7375
from b2sdk.sync.exception import UnSyncableFilename
7476
from b2sdk.sync.exception import check_invalid_argument
7577

@@ -94,12 +96,12 @@
9496
'CapExceeded',
9597
'ChecksumMismatch',
9698
'ClockSkew',
97-
'CommandError',
9899
'Conflict',
99100
'ConnectionReset',
100101
'CorruptAccountInfo',
101102
'DestFileNewer',
102103
'DuplicateBucketName',
104+
'EmptyDirectory',
103105
'EnvironmentEncodingError',
104106
'FileAlreadyHidden',
105107
'FileNameNotAllowed',
@@ -116,6 +118,7 @@
116118
'MissingAccountData',
117119
'MissingPart',
118120
'NonExistentBucket',
121+
'NotADirectory',
119122
'NotAllowedByAppKeyError',
120123
'PartSha1Mismatch',
121124
'RestrictedBucket',
@@ -130,6 +133,7 @@
130133
'UnknownError',
131134
'UnknownHost',
132135
'UnrecognizedBucketType',
136+
'UnableToCreateDirectory',
133137
'UnSyncableFilename',
134138
'UnsatisfiableRange',
135139
'UnusableFileName',

b2sdk/exception.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,6 @@ def __str__(self):
172172
)
173173

174174

175-
class CommandError(B2Error):
176-
"""
177-
b2 command error (user caused). Accepts exactly one argument: message.
178-
179-
We expect users of shell scripts will parse our ``__str__`` output.
180-
"""
181-
182-
def __init__(self, message):
183-
super(CommandError, self).__init__()
184-
self.message = message
185-
186-
def __str__(self):
187-
return self.message
188-
189-
190175
class Conflict(B2SimpleError):
191176
pass
192177

b2sdk/sync/exception.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,21 @@ def check_invalid_argument(parameter_name: str, message: str,
8989
if not message:
9090
message = str(exc)
9191
raise InvalidArgument(parameter_name, message) from exc
92+
93+
94+
class BaseDirectoryError(B2SimpleError):
95+
def __init__(self, path):
96+
self.path = path
97+
super().__init__(path)
98+
99+
100+
class EmptyDirectory(BaseDirectoryError):
101+
pass
102+
103+
104+
class UnableToCreateDirectory(BaseDirectoryError):
105+
pass
106+
107+
108+
class NotADirectory(BaseDirectoryError):
109+
pass

b2sdk/sync/folder.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
import sys
1616

1717
from abc import ABCMeta, abstractmethod
18-
from b2sdk.exception import CommandError
19-
from .exception import EnvironmentEncodingError, UnSyncableFilename
18+
from .exception import EmptyDirectory, EnvironmentEncodingError, UnSyncableFilename, NotADirectory, UnableToCreateDirectory
2019
from .file import File, B2File, FileVersion, B2FileVersion
2120
from .scan_policies import DEFAULT_SCAN_MANAGER
2221
from ..utils import fix_windows_path_limit, get_file_mtime, is_file_readable
@@ -166,9 +165,9 @@ def ensure_present(self):
166165
try:
167166
os.mkdir(self.root)
168167
except OSError:
169-
raise Exception('unable to create directory %s' % (self.root,))
168+
raise UnableToCreateDirectory(self.root)
170169
elif not os.path.isdir(self.root):
171-
raise Exception('%s is not a directory' % (self.root,))
170+
raise NotADirectory(self.root)
172171

173172
def ensure_non_empty(self):
174173
"""
@@ -177,9 +176,7 @@ def ensure_non_empty(self):
177176
self.ensure_present()
178177

179178
if not os.listdir(self.root):
180-
raise CommandError(
181-
'Directory %s is empty. Use --allowEmptySource to sync anyway.' % (self.root,)
182-
)
179+
raise EmptyDirectory(self.root)
183180

184181
@classmethod
185182
def _walk_relative_paths(cls, local_dir, b2_dir, reporter, policies_manager):

b2sdk/sync/folder_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#
99
######################################################################
1010

11-
from ..exception import CommandError
11+
from .exception import InvalidArgument
1212
from .folder import B2Folder, LocalFolder
1313

1414

@@ -44,7 +44,7 @@ def _parse_bucket_and_folder(bucket_and_path, api, b2_folder_class):
4444
Turn 'my-bucket/foo' into B2Folder(my-bucket, foo).
4545
"""
4646
if '//' in bucket_and_path:
47-
raise CommandError("'//' not allowed in path names")
47+
raise InvalidArgument('folder_name', "'//' not allowed in path names")
4848
if '/' not in bucket_and_path:
4949
bucket_name = bucket_and_path
5050
folder_name = ''

b2sdk/v1/exception.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,19 @@
99
######################################################################
1010

1111
from b2sdk._v2.exception import * # noqa
12+
13+
14+
# This exception class is deprecated and should not be used in new designs
15+
class CommandError(B2Error):
16+
"""
17+
b2 command error (user caused). Accepts exactly one argument: message.
18+
19+
We expect users of shell scripts will parse our ``__str__`` output.
20+
"""
21+
22+
def __init__(self, message):
23+
super(CommandError, self).__init__()
24+
self.message = message
25+
26+
def __str__(self):
27+
return self.message

b2sdk/v1/sync/folder.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,28 @@
99
######################################################################
1010

1111
from abc import abstractmethod
12+
import functools
1213

1314
from b2sdk import _v2 as v2
1415
from .scan_policies import DEFAULT_SCAN_MANAGER
16+
from .. import exception
17+
18+
19+
def translate_errors(func):
20+
@functools.wraps(func)
21+
def wrapper(*a, **kw):
22+
try:
23+
return func(*a, **kw)
24+
except exception.NotADirectory as ex:
25+
raise Exception('%s is not a directory' % (ex.path,))
26+
except exception.UnableToCreateDirectory as ex:
27+
raise Exception('unable to create directory %s' % (ex.path,))
28+
except exception.EmptyDirectory as ex:
29+
raise exception.CommandError(
30+
'Directory %s is empty. Use --allowEmptySource to sync anyway.' % (ex.path,)
31+
)
32+
33+
return wrapper
1534

1635

1736
# Override to change "policies_manager" default argument
@@ -21,9 +40,16 @@ def all_files(self, reporter, policies_manager=DEFAULT_SCAN_MANAGER):
2140
pass
2241

2342

24-
class LocalFolder(v2.LocalFolder, AbstractFolder):
43+
class B2Folder(v2.B2Folder, AbstractFolder):
2544
pass
2645

2746

28-
class B2Folder(v2.B2Folder, AbstractFolder):
29-
pass
47+
# "policies_manager" default argument and translate nice errors to old style Exceptions and CommandError
48+
class LocalFolder(v2.LocalFolder, AbstractFolder):
49+
@translate_errors
50+
def ensure_present(self):
51+
return super().ensure_present()
52+
53+
@translate_errors
54+
def ensure_non_empty(self):
55+
return super().ensure_non_empty()

b2sdk/v1/sync/folder_parser.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
######################################################################
1010

1111
from b2sdk import _v2 as v2
12+
from .. import exception
1213

1314
from .folder import LocalFolder, B2Folder
1415

1516

16-
# Override to use v1 version of "LocalFolder" and "B2Folder"
17+
# Override to use v1 version of "LocalFolder" and "B2Folder" and raise old style CommandError
1718
def parse_sync_folder(folder_name, api):
18-
return v2.parse_sync_folder(folder_name, api, LocalFolder, B2Folder)
19+
try:
20+
return v2.parse_sync_folder(folder_name, api, LocalFolder, B2Folder)
21+
except exception.InvalidArgument as ex:
22+
raise exception.CommandError(ex.message)

test/unit/sync/test_sync.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from functools import partial
1414

1515
from apiver_deps import B2DownloadAction, B2UploadAction, B2CopyAction, AbstractSyncEncryptionSettingsProvider, UploadSourceLocalFile
16-
from apiver_deps_exception import CommandError, DestFileNewer, InvalidArgument
16+
from apiver_deps_exception import DestFileNewer, InvalidArgument
1717
from b2sdk.utils import TempDir
1818

1919
from .fixtures import *
@@ -50,6 +50,7 @@ def assert_folder_sync_actions(self, synchronizer, src_folder, dst_folder, expec
5050
)
5151
assert expected_actions == [str(a) for a in actions]
5252

53+
@pytest.mark.apiver(to_ver=0)
5354
@pytest.mark.parametrize(
5455
'args', [
5556
{
@@ -64,10 +65,28 @@ def assert_folder_sync_actions(self, synchronizer, src_folder, dst_folder, expec
6465
'keep_days_or_delete',
6566
]
6667
)
67-
def test_illegal_args(self, synchronizer_factory, apiver, args):
68-
exceptions = defaultdict(lambda: InvalidArgument, v0=CommandError) # noqa
68+
def test_illegal_args_up_to_v0(self, synchronizer_factory, apiver, args):
69+
from apiver_deps_exception import CommandError
70+
with pytest.raises(CommandError):
71+
synchronizer_factory(**args)
6972

70-
with pytest.raises(exceptions[apiver]):
73+
@pytest.mark.apiver(from_ver=1)
74+
@pytest.mark.parametrize(
75+
'args', [
76+
{
77+
'newer_file_mode': IllegalEnum.ILLEGAL
78+
},
79+
{
80+
'keep_days_or_delete': IllegalEnum.ILLEGAL
81+
},
82+
],
83+
ids=[
84+
'newer_file_mode',
85+
'keep_days_or_delete',
86+
]
87+
)
88+
def test_illegal_args_up_v1_and_up(self, synchronizer_factory, apiver, args):
89+
with pytest.raises(InvalidArgument):
7190
synchronizer_factory(**args)
7291

7392
def test_illegal(self, synchronizer):

test/unit/test_exception.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
#
99
######################################################################
1010

11+
import pytest
12+
1113
from b2sdk.exception import FileOrBucketNotFound, ResourceNotFound
1214
from apiver_deps_exception import (
1315
AlreadyFailed,
1416
B2Error,
1517
BadJson,
1618
BadUploadUrl,
1719
CapExceeded,
18-
CommandError,
1920
Conflict,
2021
DuplicateBucketName,
2122
FileAlreadyHidden,
@@ -56,7 +57,9 @@ def test_already_failed_exception(self):
5657
except AlreadyFailed as e:
5758
assert str(e) == 'Already failed: foo', str(e)
5859

60+
@pytest.mark.apiver(to_ver=1)
5961
def test_command_error(self):
62+
from apiver_deps_exception import CommandError
6063
try:
6164
raise CommandError('foo')
6265
except CommandError as e:

0 commit comments

Comments
 (0)