Skip to content

Commit 0d3d93b

Browse files
Merge pull request #76 from xenserver-next/accessor-openText-contextmanager
Add Accessor.openText() as context manager for openAddress()
2 parents c6ee4e0 + eb14ba0 commit 0d3d93b

File tree

5 files changed

+80
-16
lines changed

5 files changed

+80
-16
lines changed

tests/test_ftpaccessor.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
"""
23
Test module of xcp.accessor.FTPAccessor
34
@@ -21,18 +22,29 @@
2122

2223
import pytest
2324
import pytest_localftpserver # pylint: disable=unused-import # Ensure that it is installed
25+
from six import ensure_binary, ensure_str
2426

2527
import xcp.accessor
2628

2729
binary_data = b"\x80\x91\xaa\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xcc\xdd\xee\xff"
30+
text_data = "✋➔Hello Accessor from the 🗺, download and verify ✅ me!"
2831

2932

33+
def upload_textfile(ftpserver, accessor):
34+
accessor.writeFile(BytesIO(ensure_binary(text_data)), "textfile")
35+
assert accessor.access("textfile")
36+
assert text_data == ensure_str(ftpserver_content(ftpserver, "testdir/textfile"))
37+
3038
def upload_binary_file(ftpserver, accessor):
3139
"""Upload a binary file and compare the uploaded file content with the local content"""
3240
accessor.writeFile(BytesIO(binary_data), "filename")
3341
assert accessor.access("filename")
34-
ftp_content_generator = ftpserver.get_file_contents("testdir/filename", read_mode="rb")
35-
assert binary_data == next(ftp_content_generator)["content"]
42+
assert binary_data == ftpserver_content(ftpserver, "testdir/filename")
43+
44+
45+
def ftpserver_content(ftpserver, path):
46+
ftp_content_generator = ftpserver.get_file_contents(path, read_mode="rb")
47+
return next(ftp_content_generator)["content"]
3648

3749

3850
@pytest.fixture
@@ -44,6 +56,7 @@ def ftp_accessor(ftpserver):
4456
accessor = xcp.accessor.FTPAccessor(url + "/testdir", False)
4557
accessor.start()
4658
upload_binary_file(ftpserver, accessor)
59+
upload_textfile(ftpserver, accessor)
4760
# This leaves ftp_accessor.finish() to each test to because disconnecting from the
4861
# ftpserver after the test in the fixture would cause the formatting of the pytest
4962
# live log to be become less readable:
@@ -73,3 +86,10 @@ def test_download_binary_file(ftp_accessor):
7386
assert remote_ftp_filehandle.read() == binary_data
7487
assert ftp_accessor.access("filename") is True # covers FTPAccessor._cleanup()
7588
ftp_accessor.finish()
89+
90+
91+
def test_download_textfile(ftp_accessor):
92+
"""Download a text file containing UTF-8 and compare the returned decoded string contents"""
93+
with ftp_accessor.openText("textfile") as remote_ftp_filehandle:
94+
assert remote_ftp_filehandle.read() == text_data
95+
ftp_accessor.finish()

tests/test_httpaccessor.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Test xcp.accessor.HTTPAccessor using a local pure-Python http(s)server fixture"""
2+
# -*- coding: utf-8 -*-
23
import base64
34
import sys
45
import unittest
6+
from contextlib import contextmanager
57

68
import pytest
79

@@ -17,6 +19,8 @@
1719
pytest.skip(allow_module_level=True)
1820

1921

22+
UTF8TEXT_LITERAL = "✋Hello accessor from the 🗺, download and verify me! ✅"
23+
2024
class HTTPAccessorTestCase(unittest.TestCase):
2125
document_root = "tests/"
2226
httpserver = HTTPServer() # pyright: ignore[reportUnboundVariable]
@@ -52,14 +56,19 @@ def handle_get(request):
5256

5357
cls.httpserver.expect_request("/" + read_file).respond_with_handler(handle_get)
5458

55-
def assert_http_get_request_data(self, url, read_file, error_handler):
59+
@contextmanager
60+
def http_get_request_data(self, url, read_file, error_handler):
5661
"""Serve a GET request, assert that the accessor returns the content of the GET Request"""
5762
self.serve_a_get_request(self.document_root, read_file, error_handler)
5863

5964
httpaccessor = createAccessor(url, True)
6065
self.assertEqual(type(httpaccessor), HTTPAccessor)
6166

6267
with open(self.document_root + read_file, "rb") as ref:
68+
yield httpaccessor, ref
69+
70+
def assert_http_get_request_data(self, url, read_file, error_handler):
71+
with self.http_get_request_data(url, read_file, error_handler) as (httpaccessor, ref):
6372
http_accessor_filehandle = httpaccessor.openAddress(read_file)
6473
if sys.version_info >= (3, 0):
6574
assert isinstance(http_accessor_filehandle, HTTPResponse)
@@ -109,3 +118,10 @@ def test_get_binary(self):
109118
+ ".pyc"
110119
)
111120
self.assert_http_get_request_data(self.httpserver.url_for(""), binary, None)
121+
122+
def test_httpaccessor_open_text(self):
123+
"""Get text containing UTF-8 and compare the returned decoded string contents"""
124+
self.httpserver.expect_request("/textfile").respond_with_data(UTF8TEXT_LITERAL)
125+
accessor = createAccessor(self.httpserver.url_for("/"), True)
126+
with accessor.openText("textfile") as textfile:
127+
assert textfile.read() == UTF8TEXT_LITERAL

tests/test_mountingaccessor.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import xcp.accessor
1010

11+
from .test_httpaccessor import UTF8TEXT_LITERAL
12+
1113
binary_data = b"\x00\x1b\x5b\x95\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xcc\xdd\xee\xff"
1214

1315

@@ -41,6 +43,7 @@ def check_mounting_accessor(accessor, fs):
4143

4244
assert check_binary_read(accessor, location, fs)
4345
assert check_binary_write(accessor, location, fs)
46+
assert open_text(accessor, location, fs, UTF8TEXT_LITERAL) == UTF8TEXT_LITERAL
4447

4548
if sys.version_info.major >= 3:
4649
fs.mount_points.pop(location)
@@ -84,3 +87,16 @@ def check_binary_write(accessor, location, fs):
8487

8588
with FakeFileOpen(fs, delete_on_close=True)(location + "/" + name, "rb") as written:
8689
return cast(bytes, written.read()) == binary_data
90+
91+
92+
def open_text(accessor, location, fs, text):
93+
# type: (xcp.accessor.MountingAccessor, str, FakeFilesystem, str) -> str
94+
"""Test the openText() method of subclasses of xcp.accessor.MountingAccessor"""
95+
name = "textfile"
96+
path = location + "/" + name
97+
assert fs.create_file(path, contents=text)
98+
assert accessor.access(name)
99+
with accessor.openText(name) as textfile:
100+
assert not isinstance(textfile, bool)
101+
fs.remove(path)
102+
return textfile.read()

xcp/accessor.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,22 @@
2525

2626
"""accessor - provide common interface to access methods"""
2727

28+
# pyre-ignore-all-errors[6,16]
2829
import ftplib
30+
import io
2931
import os
32+
import sys
3033
import tempfile
3134
import errno
35+
from contextlib import contextmanager
3236
from typing import cast, TYPE_CHECKING
3337

3438
from six.moves import urllib # pyright: ignore
3539

3640
from xcp import logger, mount
3741

3842
if TYPE_CHECKING:
43+
from collections.abc import Generator
3944
from typing import IO
4045
from typing_extensions import Literal
4146

@@ -70,6 +75,22 @@ def access(self, name):
7075

7176
return True
7277

78+
@contextmanager
79+
def openText(self, address):
80+
# type:(str) -> Generator[IO[str] | Literal[False], None, None]
81+
"""Context manager to read text from address using 'with'. Yields IO[str] or False"""
82+
readbuffer = self.openAddress(address)
83+
84+
if readbuffer and sys.version_info >= (3, 0):
85+
textiowrapper = io.TextIOWrapper(readbuffer, encoding="utf-8")
86+
yield textiowrapper
87+
textiowrapper.close()
88+
else:
89+
yield cast(io.TextIOWrapper, readbuffer)
90+
91+
if readbuffer:
92+
readbuffer.close()
93+
7394
def openAddress(self, address):
7495
# type:(str) -> IO[bytes] | Literal[False]
7596
"""must be overloaded"""

xcp/repository.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@
2424
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2525

2626
from hashlib import md5
27-
import io
2827
import os.path
2928
import xml.dom.minidom
3029
import configparser
31-
import sys
3230

3331
import six
3432

@@ -180,18 +178,11 @@ def _getVersion(cls, access, category):
180178

181179
access.start()
182180
try:
183-
rawtreeinfofp = access.openAddress(cls.TREEINFO_FILENAME)
184-
if sys.version_info < (3, 0) or isinstance(rawtreeinfofp, io.TextIOBase):
185-
# e.g. with FileAccessor
186-
treeinfofp = rawtreeinfofp
187-
else:
188-
# e.g. with HTTPAccessor
189-
treeinfofp = io.TextIOWrapper(rawtreeinfofp, encoding='utf-8')
190181
treeinfo = configparser.ConfigParser()
191-
treeinfo.read_file(treeinfofp)
192-
if treeinfofp != rawtreeinfofp:
193-
treeinfofp.close()
194-
rawtreeinfofp.close()
182+
183+
with access.openText(cls.TREEINFO_FILENAME) as fp:
184+
treeinfo.read_file(fp)
185+
195186
if treeinfo.has_section('system-v1'):
196187
ver_str = treeinfo.get('system-v1', category_map[category])
197188
else:

0 commit comments

Comments
 (0)