Skip to content

Commit 0174bb7

Browse files
committed
Improve API of 'git_pw.api' module
There were too many public things being exposed by this module. Clean things up a little. Signed-off-by: Stephen Finucane <stephen@that.guru>
1 parent 84a12bc commit 0174bb7

File tree

7 files changed

+91
-131
lines changed

7 files changed

+91
-131
lines changed

git_pw/api.py

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import os.path
88
import re
9+
import pty
910
import sys
1011
import tempfile
1112

@@ -17,6 +18,7 @@
1718

1819
if 0: # noqa
1920
from typing import Dict # noqa
21+
from typing import IO # noqa
2022
from typing import List # noqa
2123
from typing import Optional # noqa
2224
from typing import Tuple # noqa
@@ -121,20 +123,7 @@ def _handle_error(operation, exc):
121123
sys.exit(1)
122124

123125

124-
def version():
125-
# type: () -> Optional[Tuple[int, int]]
126-
"""Get the version of the server from the URL, if present."""
127-
server = _get_server()
128-
129-
version = re.match(r'.*/(\d)\.(\d)$', server)
130-
if version:
131-
return (int(version.group(1)), int(version.group(2)))
132-
133-
# return the oldest version we support if no version provided
134-
return (1, 0)
135-
136-
137-
def get(url, params=None, stream=False):
126+
def _get(url, params=None, stream=False):
138127
# type: (str, Filters, bool) -> requests.Response
139128
"""Make GET request and handle errors."""
140129
LOG.debug('GET %s', url)
@@ -155,7 +144,7 @@ def get(url, params=None, stream=False):
155144
return rsp
156145

157146

158-
def patch(url, data):
147+
def _patch(url, data):
159148
# type: (str, dict) -> requests.Response
160149
"""Make PATCH request and handle errors."""
161150
LOG.debug('PATCH %s, data=%r', url, data)
@@ -172,39 +161,64 @@ def patch(url, data):
172161
return rsp
173162

174163

175-
def download(url, params=None):
176-
# type: (str, Filters) -> str
177-
"""Retrieve a specific API resource and save it to a file.
164+
def version():
165+
# type: () -> Optional[Tuple[int, int]]
166+
"""Get the version of the server from the URL, if present."""
167+
server = _get_server()
178168

179-
GET /{resource}/{resourceID}/
169+
version = re.match(r'.*/(\d)\.(\d)$', server)
170+
if version:
171+
return (int(version.group(1)), int(version.group(2)))
172+
173+
# return the oldest version we support if no version provided
174+
return (1, 0)
175+
176+
177+
def download(url, params=None, output=None):
178+
# type: (str, Filters, IO) -> Optional[str]
179+
"""Retrieve a specific API resource and save it to a file/stdout.
180180
181181
The ``Content-Disposition`` header is assumed to be present and
182-
will be used for the output filename.
182+
will be used for the output filename, if not writing to stdout.
183183
184184
Arguments:
185185
url: The resource URL.
186186
params: Additional parameters.
187+
output: The output file. If provided, the caller is responsible for
188+
closing. If None, a temporary file will be used.
187189
188190
Returns:
189-
A path to an output file containing the content.
191+
A path to an output file containing the content, else None if stdout
192+
used.
190193
"""
191-
rsp = get(url, params, stream=True)
194+
rsp = _get(url, params, stream=True)
192195

193196
# we don't catch anything here because we should break if these are missing
194-
header = re.search('filename=(.+)',
195-
rsp.headers.get('content-disposition') or '')
197+
header = re.search(
198+
'filename=(.+)', rsp.headers.get('content-disposition') or '',
199+
)
196200
if not header:
197201
LOG.error('Filename was expected but was not provided in response')
198202
sys.exit(1)
199203

200-
output_path = os.path.join(tempfile.mkdtemp(prefix='git-pw'),
201-
header.group(1))
204+
if output:
205+
output_path = None
206+
if output.fileno() != pty.STDOUT_FILENO:
207+
LOG.debug('Saving to %s', output.name)
208+
output_path = output.name
202209

203-
with open(output_path, 'wb') as output_file:
204-
LOG.debug('Saving to %s', output_path)
205210
# we use iter_content because patches can be binary
206211
for block in rsp.iter_content(1024):
207-
output_file.write(block)
212+
output.write(block)
213+
else:
214+
output_path = os.path.join(
215+
tempfile.mkdtemp(prefix='git-pw'), header.group(1),
216+
)
217+
with open(output_path, 'wb') as output_file:
218+
LOG.debug('Saving to %s', output_path)
219+
# we use iter_content because patches can be binary
220+
for block in rsp.iter_content(1024):
221+
output_file.write(block)
208222

209223
return output_path
210224

@@ -233,7 +247,7 @@ def index(resource_type, params=None):
233247
params = params or []
234248
params.append(('project', _get_project()))
235249

236-
return get(url, params).json()
250+
return _get(url, params).json()
237251

238252

239253
def detail(resource_type, resource_id, params=None):
@@ -253,7 +267,7 @@ def detail(resource_type, resource_id, params=None):
253267
# NOTE(stephenfin): All resources must have a trailing '/'
254268
url = '/'.join([_get_server(), resource_type, str(resource_id), ''])
255269

256-
return get(url, params, stream=False).json()
270+
return _get(url, params, stream=False).json()
257271

258272

259273
def update(resource_type, resource_id, data):
@@ -273,7 +287,7 @@ def update(resource_type, resource_id, data):
273287
# NOTE(stephenfin): All resources must have a trailing '/'
274288
url = '/'.join([_get_server(), resource_type, str(resource_id), ''])
275289

276-
return patch(url, data).json()
290+
return _patch(url, data).json()
277291

278292

279293
def validate_multiple_filter_support(f):

git_pw/bundle.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
import logging
6-
import pty
76
import sys
87

98
import click
@@ -72,15 +71,7 @@ def download_cmd(bundle_id, output):
7271
path = None
7372
bundle = _get_bundle(bundle_id)
7473

75-
if output:
76-
content = api.get(bundle['mbox']).content
77-
78-
output.write(content)
79-
80-
if output.fileno() != pty.STDOUT_FILENO:
81-
path = output.name
82-
else:
83-
path = api.download(bundle['mbox'])
74+
path = api.download(bundle['mbox'], output=output)
8475

8576
if path:
8677
LOG.info('Downloaded bundle to %s', path)

git_pw/patch.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,24 +75,20 @@ def download_cmd(patch_id, output, fmt):
7575
path = None
7676
patch = api.detail('patches', patch_id)
7777

78-
if output:
79-
if fmt == 'diff':
80-
content = patch['diff']
78+
if fmt == 'diff':
79+
if output:
80+
output.write(patch['diff'])
81+
if output.fileno() != pty.STDOUT_FILENO:
82+
path = output.name
8183
else:
82-
content = api.get(patch['mbox']).content
83-
84-
output.write(content)
85-
86-
if output.fileno() != pty.STDOUT_FILENO:
87-
path = output.name
88-
else:
89-
if fmt == 'diff':
9084
# TODO(stephenfin): We discard the 'diff' field so we can get the
9185
# filename and save to the correct file. We should expose this
9286
# information via the API
93-
path = api.download(patch['mbox'].replace('mbox', 'raw'))
94-
else:
95-
path = api.download(patch['mbox'])
87+
path = api.download(
88+
patch['mbox'].replace('mbox', 'raw'), output=output,
89+
)
90+
else:
91+
path = api.download(patch['mbox'], output=output)
9692

9793
if path:
9894
LOG.info('Downloaded patch to %s', path)

git_pw/series.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
import logging
6-
import pty
76

87
import arrow
98
import click
@@ -51,15 +50,7 @@ def download_cmd(series_id, output):
5150
path = None
5251
series = api.detail('series', series_id)
5352

54-
if output:
55-
content = api.get(series['mbox']).content
56-
57-
output.write(content)
58-
59-
if output.fileno() != pty.STDOUT_FILENO:
60-
path = output.name
61-
else:
62-
path = api.download(series['mbox'])
53+
path = api.download(series['mbox'], output=output)
6354

6455
if path:
6556
LOG.info('Downloaded series to %s', path)

tests/test_bundle.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22

33
from click.testing import CliRunner as CLIRunner
4+
from click import utils as click_utils
45
import mock
56

67
from git_pw import bundle
@@ -94,10 +95,9 @@ def test_apply_with_args(self, mock_git_am, mock_download,
9495

9596
@mock.patch('git_pw.bundle._get_bundle')
9697
@mock.patch('git_pw.api.download')
97-
@mock.patch('git_pw.api.get')
9898
class DownloadTestCase(unittest.TestCase):
9999

100-
def test_download(self, mock_get, mock_download, mock_get_bundle):
100+
def test_download(self, mock_download, mock_get_bundle):
101101
"""Validate standard behavior."""
102102

103103
rsp = {'mbox': 'http://example.com/api/patches/123/mbox/'}
@@ -109,33 +109,24 @@ def test_download(self, mock_get, mock_download, mock_get_bundle):
109109

110110
assert result.exit_code == 0, result
111111
mock_get_bundle.assert_called_once_with('123')
112-
mock_download.assert_called_once_with(rsp['mbox'])
113-
mock_get.assert_not_called()
112+
mock_download.assert_called_once_with(rsp['mbox'], output=None)
114113

115-
def test_download_to_file(self, mock_get, mock_download, mock_get_bundle):
114+
def test_download_to_file(self, mock_download, mock_get_bundle):
116115
"""Validate downloading to a file."""
117116

118-
class MockResponse(object):
119-
@property
120-
def content(self):
121-
return b'alpha-beta'
122-
123117
rsp = {'mbox': 'http://example.com/api/patches/123/mbox/'}
124118
mock_get_bundle.return_value = rsp
125-
mock_get.return_value = MockResponse()
126119

127120
runner = CLIRunner()
128-
with runner.isolated_filesystem():
129-
result = runner.invoke(bundle.download_cmd, ['123', 'test.patch'])
121+
result = runner.invoke(bundle.download_cmd, ['123', 'test.patch'])
130122

131-
assert result.exit_code == 0, result
132-
133-
with open('test.patch') as output:
134-
assert ['alpha-beta'] == output.readlines()
123+
assert result.exit_code == 0, result
135124

136125
mock_get_bundle.assert_called_once_with('123')
137-
mock_get.assert_called_once_with(rsp['mbox'])
138-
mock_download.assert_not_called()
126+
mock_download.assert_called_once_with(rsp['mbox'], output=mock.ANY)
127+
assert isinstance(
128+
mock_download.call_args[1]['output'], click_utils.LazyFile,
129+
)
139130

140131

141132
class ShowTestCase(unittest.TestCase):

tests/test_patch.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import click
44
from click.testing import CliRunner as CLIRunner
5+
from click import utils as click_utils
56
import mock
67
from packaging import version
78

@@ -92,7 +93,7 @@ def test_download(self, mock_log, mock_download, mock_detail):
9293

9394
assert result.exit_code == 0, result
9495
mock_detail.assert_called_once_with('patches', 123)
95-
mock_download.assert_called_once_with(rsp['mbox'])
96+
mock_download.assert_called_once_with(rsp['mbox'], output=None)
9697
assert mock_log.info.called
9798

9899
def test_download_diff(self, mock_log, mock_download, mock_detail):
@@ -106,42 +107,30 @@ def test_download_diff(self, mock_log, mock_download, mock_detail):
106107

107108
assert result.exit_code == 0, result
108109
mock_detail.assert_called_once_with('patches', 123)
109-
mock_download.assert_called_once_with(rsp['mbox'].replace(
110-
'mbox', 'raw'))
110+
mock_download.assert_called_once_with(
111+
rsp['mbox'].replace('mbox', 'raw'), output=None,
112+
)
111113
assert mock_log.info.called
112114

113-
@mock.patch('git_pw.api.get')
114-
def test_download_to_file(self, mock_get, mock_log, mock_download,
115-
mock_detail):
115+
def test_download_to_file(self, mock_log, mock_download, mock_detail):
116116
"""Validate behavior if downloading to a specific file."""
117117

118-
class MockResponse(object):
119-
@property
120-
def content(self):
121-
return b'alpha-beta'
122-
123118
rsp = {'mbox': 'hello, world', 'diff': 'test'}
124119
mock_detail.return_value = rsp
125-
mock_get.return_value = MockResponse()
126120

127121
runner = CLIRunner()
128-
with runner.isolated_filesystem():
129-
result = runner.invoke(patch.download_cmd,
130-
['123', 'test.patch'])
122+
result = runner.invoke(patch.download_cmd, ['123', 'test.patch'])
131123

132-
assert result.exit_code == 0, result
133-
134-
with open('test.patch') as output:
135-
assert ['alpha-beta'] == output.readlines()
124+
assert result.exit_code == 0, result
136125

137126
mock_detail.assert_called_once_with('patches', 123)
138-
mock_download.assert_not_called()
139-
mock_get.assert_called_once_with(rsp['mbox'])
127+
mock_download.assert_called_once_with(rsp['mbox'], output=mock.ANY)
128+
assert isinstance(
129+
mock_download.call_args[1]['output'], click_utils.LazyFile,
130+
)
140131
assert mock_log.info.called
141132

142-
@mock.patch('git_pw.api.get')
143-
def test_download_diff_to_file(self, mock_get, mock_log, mock_download,
144-
mock_detail):
133+
def test_download_diff_to_file(self, mock_log, mock_download, mock_detail):
145134
"""Validate behavior if downloading a diff to a specific file."""
146135

147136
rsp = {'mbox': 'hello, world', 'diff': b'test'}
@@ -159,7 +148,6 @@ def test_download_diff_to_file(self, mock_get, mock_log, mock_download,
159148

160149
mock_detail.assert_called_once_with('patches', 123)
161150
mock_download.assert_not_called()
162-
mock_get.assert_not_called()
163151
assert mock_log.info.called
164152

165153

0 commit comments

Comments
 (0)