From e541654a10cb5ae72ad34efc9fe0ddb6b4bd1022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 30 May 2020 19:13:56 +0000 Subject: [PATCH 1/3] Enable GitHub actions on Windows. --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 894a594742..fb8fe3eb15 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, windows-latest] python-version: [3.6, 3.7, 3.8] experimental: [false] # See https://github.com/actions/toolkit/issues/399 From ed73446440e8dcc3f6ba312e5edc4e6c28927c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 23 Jan 2022 15:52:29 +0000 Subject: [PATCH 2/3] Only install pyinotify on Linux. --- .github/workflows/pythonpackage.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index fb8fe3eb15..8ddbfee2fd 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -33,7 +33,11 @@ jobs: run: | python -m pip install --upgrade pip pip install -U pip setuptools - pip install -U pip coverage codecov fastbencode flake8 testtools paramiko fastimport configobj cython testscenarios six docutils $TEST_REQUIRE sphinx sphinx_epytext launchpadlib patiencediff pyinotify git+https://github.com/dulwich/dulwich + pip install -U pip coverage codecov fastbencode flake8 testtools paramiko fastimport configobj cython testscenarios six docutils $TEST_REQUIRE sphinx sphinx_epytext launchpadlib patiencediff git+https://github.com/dulwich/dulwich + - name: Dependencies (Linux) + run: | + pip install -U pyinotify + if: "matrix.os == 'ubuntu-latest'" - name: Build docs run: | make docs PYTHON=python From 8f09d90009fb8555ed1a4f51b2bff36e10ff3020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 23 Jan 2022 16:37:30 +0000 Subject: [PATCH 3/3] Import a1s's windows fixes. --- breezy/_walkdirs_win32.pyx | 36 +++++---- breezy/osutils.py | 3 +- breezy/plugins/upload/tests/test_upload.py | 13 +++ breezy/tests/__init__.py | 16 ++-- breezy/tests/test__walkdirs_win32.py | 29 +++---- breezy/tests/test_bedding.py | 9 +++ breezy/tests/test_config.py | 61 +++++++++----- breezy/tests/test_osutils.py | 92 +++++++++++++++------- breezy/transport/local.py | 8 +- 9 files changed, 178 insertions(+), 89 deletions(-) diff --git a/breezy/_walkdirs_win32.pyx b/breezy/_walkdirs_win32.pyx index 40d3052677..711795c150 100644 --- a/breezy/_walkdirs_win32.pyx +++ b/breezy/_walkdirs_win32.pyx @@ -58,14 +58,11 @@ cdef extern from "python-compat.h": int GetLastError() - # Wide character functions - DWORD wcslen(WCHAR *) - cdef extern from "Python.h": - WCHAR *PyUnicode_AS_UNICODE(object) - Py_ssize_t PyUnicode_GET_SIZE(object) - object PyUnicode_FromUnicode(WCHAR *, Py_ssize_t) + WCHAR *PyUnicode_AsWideCharString(object, Py_ssize_t *size) + Py_ssize_t PyUnicode_GET_LENGTH(object) + object PyUnicode_FromWideChar(WCHAR *, Py_ssize_t) int PyList_Append(object, object) except -1 object PyUnicode_AsUTF8String(object) @@ -120,12 +117,6 @@ cdef class _Win32Stat: self.st_mtime, self.st_ctime)) -cdef object _get_name(WIN32_FIND_DATAW *data): - """Extract the Unicode name for this file/dir.""" - return PyUnicode_FromUnicode(data.cFileName, - wcslen(data.cFileName)) - - cdef int _get_mode_bits(WIN32_FIND_DATAW *data): # cannot_raise cdef int mode_bits @@ -213,25 +204,36 @@ cdef class Win32ReadDir: def read_dir(self, prefix, top): """Win32 implementation of DirReader.read_dir. + :param prefix: A utf8 prefix to be preprended to the path basenames. + :param top: A Unicode or UTF8-encoded path to read. + + :return: A list of the directories contents. Each item contains: + (utf8_relpath, utf8_name, kind, lstatvalue, unicode_abspath) + :seealso: DirReader.read_dir + """ cdef WIN32_FIND_DATAW search_data cdef HANDLE hFindFile cdef int last_err cdef WCHAR *query cdef int result + cdef Py_ssize_t length + global osutils + if osutils is None: + from . import osutils if prefix: - relprefix = prefix + '/' + relprefix = osutils.safe_utf8(prefix) + b'/' else: - relprefix = '' - top_slash = top + '/' + relprefix = b'' + top_slash = osutils.safe_unicode(top) + '/' top_star = top_slash + '*' dirblock = [] - query = PyUnicode_AS_UNICODE(top_star) + query = PyUnicode_AsWideCharString(top_star, &length) hFindFile = FindFirstFileW(query, &search_data) if hFindFile == INVALID_HANDLE_VALUE: # Raise an exception? This path doesn't seem to exist @@ -244,7 +246,7 @@ cdef class Win32ReadDir: if _should_skip(&search_data): result = FindNextFileW(hFindFile, &search_data) continue - name_unicode = _get_name(&search_data) + name_unicode = PyUnicode_FromWideChar(search_data.cFileName, -1) name_utf8 = PyUnicode_AsUTF8String(name_unicode) PyList_Append(dirblock, (relprefix + name_utf8, name_utf8, diff --git a/breezy/osutils.py b/breezy/osutils.py index 423d30cf6a..2ab6ac6c29 100644 --- a/breezy/osutils.py +++ b/breezy/osutils.py @@ -24,6 +24,7 @@ from .lazy_import import lazy_import lazy_import(globals(), """ +import ctypes from datetime import datetime import getpass import locale @@ -1304,7 +1305,7 @@ def _cicp_canonical_relpath(base, path): # filesystems), for example, so could probably benefit from the same basic # support there. For now though, only Windows and OSX get that support, and # they get it for *all* file-systems! -if sys.platform in ('win32', 'darwin'): +if sys.platform in ('win32', 'darwin', 'cygwin'): canonical_relpath = _cicp_canonical_relpath else: canonical_relpath = relpath diff --git a/breezy/plugins/upload/tests/test_upload.py b/breezy/plugins/upload/tests/test_upload.py index 1ccb10ef07..607b1bc623 100644 --- a/breezy/plugins/upload/tests/test_upload.py +++ b/breezy/plugins/upload/tests/test_upload.py @@ -109,6 +109,16 @@ def assertUpFileEqual(self, content, path, base=upload_dir): self.assertFileEqual(content, osutils.pathjoin(base, path)) def assertUpPathModeEqual(self, path, expected_mode, base=upload_dir): + # FIXME: at present, the upload is tested locally, + # so if local FS doesn't support the mode bits, + # all mode tests will fail. The only mode bit + # that is reported as unsupported by the osutils module + # is the executable bit. So skip if not supports_executable. + # This should better be tested with a dummy transport + # and not an actual file system. + if not osutils.supports_executable(path): + self.skipTest("Cannot determine mode bits for %s" % path) + return # FIXME: the tests needing that assertion should depend on the server # ability to handle chmod so that they don't fail (or be skipped) # against servers that can't. Note that some breezy transports define @@ -387,6 +397,9 @@ def test_change_dir_into_file(self): self.assertUpFileEqual(b'bar', 'hello') def _test_make_file_executable(self, file_name): + if not osutils.supports_executable(file_name) + self.skipTest("%s cannot be marked executable" % filename) + return self.make_branch_and_working_tree() self.add_file(file_name, b'foo') self.chmod_file(file_name, 0o664) diff --git a/breezy/tests/__init__.py b/breezy/tests/__init__.py index cff7cf5a12..9999e72fbd 100644 --- a/breezy/tests/__init__.py +++ b/breezy/tests/__init__.py @@ -2848,7 +2848,15 @@ def setUp(self): def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) - with open(filename, 'rb') as f: + # This seems to be called for text files only, + # but most of the existing tests check the contents + # against binary strings. Now there are cases + # when we need to normalize the line endings; expect a string then. + if isinstance(expect, str): + args = {"mode": "rt", "encoding": "utf-8"} + else: + args = {"mode": "rb"} + with open(filename, **args) as f: contents = f.read() if contents != expect: self.log("expected: %r" % expect) @@ -4467,11 +4475,9 @@ def _rmtree_temp_dir(dirname, test_id=None): if test_id is not None: ui.ui_factory.clear_term() sys.stderr.write('\nWhile running: %s\n' % (test_id,)) - # Ugly, but the last thing we want here is fail, so bear with it. - printable_e = str(e).decode(osutils.get_user_encoding(), 'replace' - ).encode('ascii', 'replace') + # 21-nov-2021 [a1s] Revert revision 5231: in Python3, str has no decode sys.stderr.write('Unable to remove testing dir %s\n%s' - % (os.path.basename(dirname), printable_e)) + % (os.path.basename(dirname), e)) def probe_unicode_in_user_encoding(): diff --git a/breezy/tests/test__walkdirs_win32.py b/breezy/tests/test__walkdirs_win32.py index 2dd99ffe12..684c48706f 100644 --- a/breezy/tests/test__walkdirs_win32.py +++ b/breezy/tests/test__walkdirs_win32.py @@ -37,9 +37,7 @@ class TestWin32Finder(tests.TestCaseInTempDir): def setUp(self): super(TestWin32Finder, self).setUp() - from ._walkdirs_win32 import ( - Win32ReadDir, - ) + from .._walkdirs_win32 import Win32ReadDir self.reader = Win32ReadDir() def _remove_stat_from_dirblock(self, dirblock): @@ -58,45 +56,44 @@ def assertWalkdirs(self, expected, top, prefix=''): finally: osutils._selected_dir_reader = old_selected_dir_reader - def assertReadDir(self, expected, prefix, top_unicode): + def assertReadDir(self, expected, prefix, top): result = self._remove_stat_from_dirblock( - self.reader.read_dir(prefix, top_unicode)) + self.reader.read_dir(prefix, top)) self.assertEqual(expected, result) def test_top_prefix_to_starting_dir(self): # preparing an iteration should create a unicode native path. self.assertEqual( - ('prefix', None, None, None, u'\x12'), + (b'prefix', None, None, None, u'\x12'), self.reader.top_prefix_to_starting_dir( u'\x12'.encode('utf8'), 'prefix')) def test_empty_directory(self): - self.assertReadDir([], 'prefix', u'.') - self.assertWalkdirs([(('', u'.'), [])], u'.') + self.assertReadDir([], b'prefix', b'.') + self.assertWalkdirs([((b'', u'.'), [])], b'.') def test_file(self): self.build_tree(['foo']) - self.assertReadDir([('foo', 'foo', 'file', u'./foo')], + self.assertReadDir([(b'foo', b'foo', 'file', u'./foo')], '', u'.') def test_directory(self): self.build_tree(['bar/']) - self.assertReadDir([('bar', 'bar', 'directory', u'./bar')], + self.assertReadDir([(b'bar', b'bar', 'directory', u'./bar')], '', u'.') def test_prefix(self): self.build_tree(['bar/', 'baf']) self.assertReadDir([ - ('xxx/baf', 'baf', 'file', u'./baf'), - ('xxx/bar', 'bar', 'directory', u'./bar'), + (b'xxx/baf', b'baf', 'file', u'./baf'), + (b'xxx/bar', b'bar', 'directory', u'./bar'), ], 'xxx', u'.') def test_missing_dir(self): e = self.assertRaises(WindowsError, - self.reader.read_dir, 'prefix', u'no_such_dir') - self.assertEqual(errno.ENOENT, e.errno) - self.assertEqual(3, e.winerror) + self.reader.read_dir, b'prefix', u'no_such_dir') + # 3 is ERROR_PATH_NOT_FOUND, see WinError.h self.assertEqual((3, u'no_such_dir/*'), e.args) @@ -106,7 +103,7 @@ class Test_Win32Stat(tests.TestCaseInTempDir): def setUp(self): super(Test_Win32Stat, self).setUp() - from ._walkdirs_win32 import lstat + from .._walkdirs_win32 import lstat self.win32_lstat = lstat def test_zero_members_present(self): diff --git a/breezy/tests/test_bedding.py b/breezy/tests/test_bedding.py index 2339aa8dcb..e8dcf1f5c8 100644 --- a/breezy/tests/test_bedding.py +++ b/breezy/tests/test_bedding.py @@ -134,6 +134,15 @@ def test_ensure_config_dir_exists(self): class TestDefaultMailDomain(tests.TestCaseInTempDir): """Test retrieving default domain from mailname file""" + def setUp(self): + if sys.platform == 'win32': + # The function deliberately returns None on Windows. + # Could run C:\Windows\System32\whoami.exe /upn, but that + # only works when the user is logged in to an AD domain. + # Why we don't just read /etc/maildomain, same as on all platforms? + raise tests.TestNotApplicable + super(TestDefaultMailDomain, self).setUp() + def test_default_mail_domain_simple(self): with open('simple', 'w') as f: f.write("domainname.com\n") diff --git a/breezy/tests/test_config.py b/breezy/tests/test_config.py index 2a2abd2cf2..af88c3ef9d 100644 --- a/breezy/tests/test_config.py +++ b/breezy/tests/test_config.py @@ -1012,12 +1012,12 @@ def test_config_creates_local(self): """Creating a new entry in config uses a local path.""" branch = self.make_branch('branch', format='knit') branch.set_push_location('http://foobar') - local_path = osutils.getcwd().encode('utf8') + local_path = osutils.getcwd() # Surprisingly ConfigObj doesn't create a trailing newline self.check_file_contents(bedding.locations_config_path(), - b'[%s/branch]\n' - b'push_location = http://foobar\n' - b'push_location:policy = norecurse\n' + '[%s/branch]\n' + 'push_location = http://foobar\n' + 'push_location:policy = norecurse\n' % (local_path,)) def test_autonick_urlencoded(self): @@ -3190,11 +3190,22 @@ def test_branch_name_colo(self): def test_branch_name_basename(self): store = self.get_store(self) - store._load_from_string(dedent("""\ - [/] - push_location=my{branchname} - """).encode('ascii')) - matcher = config.LocationMatcher(store, 'file:///parent/example%3c') + if sys.platform == "win32": + # Win32 file urls start with file:///x:/, + # where x is a valid drive letter + store._load_from_string(dedent("""\ + [C:/] + push_location=my{branchname} + """).encode('ascii')) + matcher = config.LocationMatcher(store, + 'file:///c:/parent/example%3c') + else: + store._load_from_string(dedent("""\ + [/] + push_location=my{branchname} + """).encode('ascii')) + matcher = config.LocationMatcher(store, + 'file:///parent/example%3c') self.assertEqual('example<', matcher.branch_name) ((_, section),) = matcher.get_sections() self.assertEqual('example<', section.locals['branchname']) @@ -3220,19 +3231,31 @@ def test_empty(self): def test_url_vs_local_paths(self): # The matcher location is an url and the section names are local paths - self.assertSectionIDs(['/foo/bar', '/foo'], - 'file:///foo/bar/baz', b'''\ -[/foo] -[/foo/bar] -''') + if sys.platform == "win32": + # Win32 file urls start with file:///x:/, + # where x is a valid drive letter + self.assertSectionIDs(['C:/foo/bar', 'C:/foo'], + 'file:///c:/foo/bar/baz', + b'[C:/foo]\n' + b'[C:/foo/bar]\n') + else: + self.assertSectionIDs(['/foo/bar', '/foo'], + 'file:///foo/bar/baz', + b'[/foo]\n' + b'[/foo/bar]\n') def test_local_path_vs_url(self): # The matcher location is a local path and the section names are urls - self.assertSectionIDs(['file:///foo/bar', 'file:///foo'], - '/foo/bar/baz', b'''\ -[file:///foo] -[file:///foo/bar] -''') + if sys.platform == "win32": + self.assertSectionIDs(['file:///C:/foo/bar', 'file:///C:/foo'], + 'C:/foo/bar/baz', + b'[file:///C:/foo]\n' + b'[file:///C:/foo/bar]\n') + else: + self.assertSectionIDs(['file:///foo/bar', 'file:///foo'], + '/foo/bar/baz', + b'[file:///foo]\n' + b'[file:///foo/bar]\n') def test_no_name_section_included_when_present(self): # Note that other tests will cover the case where the no-name section diff --git a/breezy/tests/test_osutils.py b/breezy/tests/test_osutils.py index 16fa5e28ed..415ae6abe0 100644 --- a/breezy/tests/test_osutils.py +++ b/breezy/tests/test_osutils.py @@ -879,8 +879,10 @@ def test_abspath(self): self.requireFeature(features.win32_feature) self.assertEqual('C:/foo', osutils._win32_abspath('C:\\foo')) self.assertEqual('C:/foo', osutils._win32_abspath('C:/foo')) - self.assertEqual('//HOST/path', osutils._win32_abspath(r'\\HOST\path')) - self.assertEqual('//HOST/path', osutils._win32_abspath('//HOST/path')) + self.assertEqual('//HOST/SHARE/path', + osutils._win32_abspath(r'\\HOST\share\path')) + self.assertEqual('//HOST/SHARE/path', + osutils._win32_abspath('//host/SHARE/path')) def test_realpath(self): self.assertEqual('C:/foo', osutils._win32_realpath('C:\\foo')) @@ -1071,10 +1073,30 @@ def test_split_with_carriage_returns(self): class TestWalkDirs(tests.TestCaseInTempDir): + if sys.platform == 'win32': + @staticmethod + def _normalize_path_separators(path): + return path.replace("\\", "/") + else: + @staticmethod + def _normalize_path_separators(path): + return path + def assertExpectedBlocks(self, expected, result): - self.assertEqual(expected, - [(dirinfo, [line[0:3] for line in block]) - for dirinfo, block in result]) + # This is randomly called with result obtained from _walkdirs_utf8 + # which always uses forward slashes for path separators, + # and simple walkdirs, which relies on os.scandir, + # so yields paths with backslashes on Windows. + # We don't compare the "path-from-top" members here, + # but in deeper trees it appears in the "directory-path-from-top". + # We have to normalize the slashes in that element. + _normalize_walkdirs_result = lambda spec: [( + (relpath, self._normalize_path_separators(fromtop)), + [line[:3] for line in block], + ) for ((relpath, fromtop), block) in spec] + self.assertEqual( + _normalize_walkdirs_result(expected), + _normalize_walkdirs_result(result)) def test_walkdirs(self): tree = [ @@ -1226,13 +1248,23 @@ def test__walkdirs_utf8(self): self.assertExpectedBlocks(expected_dirblocks[1:], result) def _filter_out_stat(self, result): - """Filter out the stat value from the walkdirs result""" - for dirdetail, dirblock in result: + """Filter out the stat value from the walkdirs result + + If needed, also change the directory separators + in the path-from-top elements to be forward slashes. + + """ + new_result = [] + for ((relpath, fromtop), dirblock) in result: new_dirblock = [] for info in dirblock: # Ignore info[3] which is the stat - new_dirblock.append((info[0], info[1], info[2], info[4])) - dirblock[:] = new_dirblock + new_dirblock.append((info[0], info[1], info[2], + self._normalize_path_separators(info[4]))) + new_result.append(( + (relpath, self._normalize_path_separators(fromtop)), + new_dirblock)) + return new_result def _save_platform_info(self): self.overrideAttr(osutils, '_fs_enc') @@ -1260,6 +1292,8 @@ def test_force_walkdirs_utf8_fs_ascii(self): UTF8DirReaderFeature.module.UTF8DirReader, b".") def test_force_walkdirs_utf8_fs_latin1(self): + if sys.platform == "win32": + self.skipTest("On Windows, FS functions use wide chars") self._save_platform_info() osutils._fs_enc = 'iso-8859-1' self.assertDirReaderIs(osutils.UnicodeDirReader, ".") @@ -1304,11 +1338,9 @@ def test_unicode_walkdirs(self): ] ), ] - result = list(osutils.walkdirs('.')) - self._filter_out_stat(result) + result = self._filter_out_stat(osutils.walkdirs('.')) self.assertEqual(expected_dirblocks, result) - result = list(osutils.walkdirs(u'./' + name1, name1)) - self._filter_out_stat(result) + result = self._filter_out_stat(osutils.walkdirs(u'./' + name1, name1)) self.assertEqual(expected_dirblocks[1:], result) def test_unicode__walkdirs_utf8(self): @@ -1418,8 +1450,7 @@ def test__walkdirs_utf8_with_unicode_fs(self): ] ), ] - result = list(osutils._walkdirs_utf8('.')) - self._filter_out_stat(result) + result = self._filter_out_stat(osutils._walkdirs_utf8('.')) self.assertEqual(expected_dirblocks, result) def test__walkdirs_utf8_win32readdir(self): @@ -1446,26 +1477,25 @@ def test__walkdirs_utf8_win32readdir(self): # All of the abspaths should be in unicode, all of the relative paths # should be in utf8 expected_dirblocks = [ - (('', '.'), + ((b'', '.'), [(name0, name0, 'file', './' + name0u), (name1, name1, 'directory', './' + name1u), (name2, name2, 'file', './' + name2u), ] ), ((name1, './' + name1u), - [(name1 + '/' + name0, name0, 'file', './' + name1u + [(name1 + b'/' + name0, name0, 'file', './' + name1u + '/' + name0u), - (name1 + '/' + name1, name1, 'directory', './' + name1u + (name1 + b'/' + name1, name1, 'directory', './' + name1u + '/' + name1u), ] ), - ((name1 + '/' + name1, './' + name1u + '/' + name1u), + ((name1 + b'/' + name1, './' + name1u + '/' + name1u), [ ] ), ] - result = list(osutils._walkdirs_utf8(u'.')) - self._filter_out_stat(result) + result = self._filter_out_stat(osutils._walkdirs_utf8(u'.')) self.assertEqual(expected_dirblocks, result) def assertStatIsCorrect(self, path, win32stat): @@ -1474,8 +1504,10 @@ def assertStatIsCorrect(self, path, win32stat): self.assertAlmostEqual(os_stat.st_mtime, win32stat.st_mtime, places=4) self.assertAlmostEqual(os_stat.st_ctime, win32stat.st_ctime, places=4) self.assertAlmostEqual(os_stat.st_atime, win32stat.st_atime, places=4) - self.assertEqual(os_stat.st_dev, win32stat.st_dev) - self.assertEqual(os_stat.st_ino, win32stat.st_ino) + # win32stat is built from WIN32_FIND_DATA structure + # which contains neither dwVolumeSerialNumber nor nFileIndex + #self.assertEqual(os_stat.st_dev, win32stat.st_dev) + #self.assertEqual(os_stat.st_ino, win32stat.st_ino) self.assertEqual(os_stat.st_mode, win32stat.st_mode) def test__walkdirs_utf_win32_find_file_stat_file(self): @@ -1492,7 +1524,7 @@ def test__walkdirs_utf_win32_find_file_stat_file(self): with open(name0u, 'ab') as f: f.write(b'just a small update') - result = Win32ReadDir().read_dir('', u'.') + result = Win32ReadDir().read_dir(b'', u'.') entry = result[0] self.assertEqual((name0, name0, 'file'), entry[:3]) self.assertEqual(u'./' + name0u, entry[4]) @@ -1620,13 +1652,17 @@ def test_copy_tree_handlers(self): processed_links = [] def file_handler(from_path, to_path): - processed_files.append(('f', from_path, to_path)) + processed_files.append( + ('f', osutils.normpath(from_path), osutils.normpath(to_path))) def dir_handler(from_path, to_path): - processed_files.append(('d', from_path, to_path)) + processed_files.append( + ('d', osutils.normpath(from_path), osutils.normpath(to_path))) def link_handler(from_path, to_path): - processed_links.append((from_path, to_path)) + processed_links.append( + (osutils.normpath(from_path), osutils.normpath(to_path))) + handlers = {'file': file_handler, 'directory': dir_handler, 'symlink': link_handler, @@ -1893,7 +1929,7 @@ class TestReadLink(tests.TestCaseInTempDir): def setUp(self): super(tests.TestCaseInTempDir, self).setUp() - self._test_needs_features.append(features.SymlinkFeature(self.test_dir)) + self.requireFeature(features.SymlinkFeature(self.test_dir)) self.link = u'l\N{Euro Sign}ink' self.target = u'targe\N{Euro Sign}t' os.symlink(self.target, self.link) diff --git a/breezy/transport/local.py b/breezy/transport/local.py index 7626263403..15cd82b119 100644 --- a/breezy/transport/local.py +++ b/breezy/transport/local.py @@ -30,13 +30,12 @@ from breezy import ( atomicfile, - osutils, urlutils, ) from breezy.transport import LateReadError """) -from .. import transport +from .. import osutils, transport _append_flags = os.O_CREAT | os.O_APPEND | os.O_WRONLY | osutils.O_BINARY | osutils.O_NOINHERIT @@ -154,7 +153,10 @@ def get(self, relpath): path = self._abspath(relpath) return open(path, 'rb') except (IOError, OSError) as e: - if e.errno == errno.EISDIR: + if e.errno == errno.EISDIR or ( + (sys.platform == "win32") and (e.errno == errno.EACCES) + and os.path.isdir(path) + ): return LateReadError(relpath) self._translate_error(e, path)