Build fixes and clean-up

* reflect Python 3.7 release
* fix `pycodestyle` errors
* update `pycodestyle` config
* move `pytest` and `pycodestyle` config to `setup.cfg`
* add `make pycodestyle`
* add `make coveralls`
* etc.
This commit is contained in:
Jakub Roztocil 2018-07-12 21:16:16 +02:00
parent a50660cc70
commit 7917f1b40c
18 changed files with 175 additions and 145 deletions

View File

@ -1 +0,0 @@
; needs to exist otherwise `$ coveralls` fails

4
.gitignore vendored
View File

@ -2,12 +2,12 @@
.idea/ .idea/
__pycache__/ __pycache__/
dist/ dist/
httpie.egg-info/
build/ build/
*.egg-info *.egg-info
.cache/ .cache/
.tox .tox/
.coverage .coverage
*.pyc *.pyc
*.egg *.egg
htmlcov htmlcov
.pytest_cache/

View File

@ -1,95 +1,77 @@
# https://travis-ci.org/jakubroztocil/httpie # <https://travis-ci.org/jakubroztocil/httpie>
sudo: false sudo: false
language: python language: python
os: os:
- linux - linux
env: env:
global: global:
- NEWEST_PYTHON=3.6 - NEWEST_PYTHON=3.7
python: python:
- 2.7 - 2.7
- pypy
- 3.4 - 3.4
- 3.5 - 3.5
- 3.6 - 3.6
# Currently fails because of a Flask issue - 3.7
# - pypy3 - pypy
# pypy3 currently fails because of a Flask issue
# - pypy3
cache: pip cache: pip
matrix: matrix:
include: include:
# Add manually defined OS X builds
# Manually defined OS X builds # <https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages)>
# https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages)
# Stock OSX Python
- os: osx - os: osx
language: generic language: generic
env: env:
# Stock OSX Python
- TOXENV=py27 - TOXENV=py27
- BREW_PYTHON_PACKAGE=
# Latest Python 2.x from Homebrew
- os: osx - os: osx
language: generic language: generic
env: env:
# Latest Python 2.7 from Homebrew
- TOXENV=py27 - TOXENV=py27
- BREW_INSTALL=python - BREW_PYTHON_PACKAGE=python@2
# Latest Python 3.x from Homebrew
- os: osx - os: osx
language: generic language: generic
env: env:
- TOXENV=py36 # Latest Python 3.x from Homebrew
- BREW_INSTALL=python3 - TOXENV=py37 # <= needs to be kept up-to-date to reflect latest minor version
- BREW_PYTHON_PACKAGE=python@3
# Python Codestyle # Add a codestyle-only build
- os: linux - os: linux
python: 3.6 python: 3.6
env: CODESTYLE=true env: CODESTYLE_ONLY=true
install: install:
- | - |
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
if [[ -n "$BREW_INSTALL" ]]; then sudo pip install tox
brew update if [[ -n "$BREW_PYTHON_PACKAGE" ]]; then
brew install "$BREW_INSTALL" brew install "$BREW_PYTHON_PACKAGE"
fi fi
sudo pip install tox
fi fi
if [[ $CODESTYLE ]]; then
pip install pycodestyle
fi
script: script:
- | - |
if [[ $TRAVIS_OS_NAME == 'linux' ]]; then if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
if [[ $CODESTYLE ]]; then if [[ $CODESTYLE_ONLY ]]; then
# 241 - multiple spaces after , make pycodestyle
# 501 - line too long else
pycodestyle --ignore=E241,E501 make test
else fi
make
fi
else else
PATH="/usr/local/bin:$PATH" tox -e "$TOXENV" PATH="/usr/local/bin:$PATH" tox -e "$TOXENV"
fi fi
after_success: after_success:
- | - |
if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON && $TRAVIS_OS_NAME == 'linux' ]]; then if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON && $TRAVIS_OS_NAME == 'linux' ]]; then
pip install python-coveralls && coveralls make coveralls
fi fi
notifications: notifications:
webhooks: webhooks:
# options: [always|never|change] default: always
on_success: always
on_failure: always
on_start: always
urls: urls:
# https://gitter.im/jkbrzt/httpie # https://gitter.im/jkbrzt/httpie
- https://webhooks.gitter.im/e/c42fcd359a110d02830b - https://webhooks.gitter.im/e/c42fcd359a110d02830b
on_success: always # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: always # options: [always|never|change] default: always

View File

@ -53,7 +53,8 @@ Go to https://github.com/jakubroztocil/httpie and fork the project repository.
Making Changes Making Changes
-------------- --------------
Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8). Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8)
and that ``make pycodestyle`` passes.
Testing Testing
@ -80,6 +81,9 @@ Running all tests:
# Run all tests for code as well as packaging, etc. # Run all tests for code as well as packaging, etc.
make test-all make test-all
# Test PEP8 compliance
make pycodestyle
Running specific tests: Running specific tests:
*********************** ***********************
@ -92,11 +96,11 @@ Running specific tests:
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok
# Run specific tests on the on all Pythons via Tox # Run specific tests on the on all Pythons via Tox
# (change to `tox -e py37' to limit Python version)
tox -- tests/test_uploads.py --verbose tox -- tests/test_uploads.py --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload --verbose tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose
----- -----
See `Makefile`_ for additional development utilities. See `Makefile`_ for additional development utilities.

View File

@ -1,6 +1,6 @@
# ###############################################################################
# See ./CONTRIBUTING.rst # See ./CONTRIBUTING.rst
# ###############################################################################
VERSION=$(shell grep __version__ httpie/__init__.py) VERSION=$(shell grep __version__ httpie/__init__.py)
REQUIREMENTS="requirements-dev.txt" REQUIREMENTS="requirements-dev.txt"
@ -20,6 +20,17 @@ init: uninstall-httpie
@echo @echo
clean:
@echo $(TAG)Cleaning up$(END)
rm -rf .tox *.egg dist build .coverage .cache .pytest_cache httpie.egg-info
find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
@echo
###############################################################################
# Testing
###############################################################################
test: init test: init
@echo $(TAG)Running tests on the current Python interpreter with coverage $(END) @echo $(TAG)Running tests on the current Python interpreter with coverage $(END)
@ -27,9 +38,8 @@ test: init
@echo @echo
test-tox: init # test-all is meant to test everything — even this Makefile
@echo $(TAG)Running tests on all Pythons via Tox$(END) test-all: uninstall-all clean init test test-tox test-dist pycodestyle
tox
@echo @echo
@ -37,6 +47,12 @@ test-dist: test-sdist test-bdist-wheel
@echo @echo
test-tox: init
@echo $(TAG)Running tests on all Pythons via Tox$(END)
tox
@echo
test-sdist: clean uninstall-httpie test-sdist: clean uninstall-httpie
@echo $(TAG)Testing sdist build an installation$(END) @echo $(TAG)Testing sdist build an installation$(END)
python setup.py sdist python setup.py sdist
@ -53,12 +69,26 @@ test-bdist-wheel: clean uninstall-httpie
@echo @echo
# This tests everything, even this Makefile. pycodestyle:
test-all: uninstall-all clean init test test-tox test-dist which pycodestyle || pip install pycodestyle
pycodestyle
@echo
coveralls:
which coveralls || pip install python-coveralls
coveralls
@echo
###############################################################################
# Publishing to PyPi
###############################################################################
publish: test-all publish-no-test publish: test-all publish-no-test
publish-no-test: publish-no-test:
@echo $(TAG)Testing wheel build an installation$(END) @echo $(TAG)Testing wheel build an installation$(END)
@echo "$(VERSION)" @echo "$(VERSION)"
@ -69,12 +99,10 @@ publish-no-test:
@echo @echo
clean:
@echo $(TAG)Cleaning up$(END)
rm -rf .tox *.egg dist build .coverage
find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
@echo
###############################################################################
# Uninstalling
###############################################################################
uninstall-httpie: uninstall-httpie:
@echo $(TAG)Uninstalling httpie$(END) @echo $(TAG)Uninstalling httpie$(END)
@ -96,5 +124,10 @@ uninstall-all: uninstall-httpie
- pip uninstall --yes -r $(REQUIREMENTS) - pip uninstall --yes -r $(REQUIREMENTS)
###############################################################################
# Utils
###############################################################################
homebrew-formula-vars: homebrew-formula-vars:
extras/get-homebrew-formula-vars.py extras/get-homebrew-formula-vars.py

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
""" """
Generate URLs and file hashes to be included in the Homebrew formula Generate URLs and file hashes to be included in the Homebrew formula
after a new release of HTTPie is published on PyPi. after a new release of HTTPie has been published on PyPi.
https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
@ -17,7 +17,7 @@ PACKAGES = [
] ]
def get_info(package_name): def get_package_meta(package_name):
api_url = 'https://pypi.python.org/pypi/{}/json'.format(package_name) api_url = 'https://pypi.python.org/pypi/{}/json'.format(package_name)
resp = requests.get(api_url).json() resp = requests.get(api_url).json()
hasher = hashlib.sha256() hasher = hashlib.sha256()
@ -35,21 +35,23 @@ def get_info(package_name):
'{}: download not found: {}'.format(package_name, resp)) '{}: download not found: {}'.format(package_name, resp))
packages = { def main():
package_name: get_info(package_name) for package_name in PACKAGES package_meta_map = {
} package_name: get_package_meta(package_name)
for package_name in PACKAGES
}
httpie_meta = package_meta_map.pop('httpie')
print()
print(' url "{url}"'.format(url=httpie_meta['url']))
print(' sha256 "{sha256}"'.format(sha256=httpie_meta['sha256']))
print()
for dep_meta in package_meta_map.values():
print(' resource "{name}" do'.format(name=dep_meta['name']))
print(' url "{url}"'.format(url=dep_meta['url']))
print(' sha256 "{sha256}"'.format(sha256=dep_meta['sha256']))
print(' end')
print()
httpie_info = packages.pop('httpie') if __name__ == '__main__':
print(""" main()
url "{url}"
sha256 "{sha256}"
""".format(**httpie_info))
for package_info in packages.values():
print("""
resource "{name}" do
url "{url}"
sha256 "{sha256}"
end""".format(**package_info))

View File

@ -164,8 +164,8 @@ def program(args, env, log_error):
if downloader and not downloader.finished: if downloader and not downloader.finished:
downloader.failed() downloader.failed()
if (not isinstance(args, list) and args.output_file and if (not isinstance(args, list) and args.output_file
args.output_file_specified): and args.output_file_specified):
args.output_file.close() args.output_file.close()

View File

@ -54,8 +54,8 @@ def parse_content_range(content_range, resumed_from):
raise ContentRangeError('Missing Content-Range') raise ContentRangeError('Missing Content-Range')
pattern = ( pattern = (
'^bytes (?P<first_byte_pos>\d+)-(?P<last_byte_pos>\d+)' r'^bytes (?P<first_byte_pos>\d+)-(?P<last_byte_pos>\d+)'
'/(\*|(?P<instance_length>\d+))$' r'/(\*|(?P<instance_length>\d+))$'
) )
match = re.match(pattern, content_range) match = re.match(pattern, content_range)
@ -78,15 +78,15 @@ def parse_content_range(content_range, resumed_from):
# last-byte-pos value, is invalid. The recipient of an invalid # last-byte-pos value, is invalid. The recipient of an invalid
# byte-content-range- spec MUST ignore it and any content # byte-content-range- spec MUST ignore it and any content
# transferred along with it." # transferred along with it."
if (first_byte_pos >= last_byte_pos or if (first_byte_pos >= last_byte_pos
(instance_length is not None and or (instance_length is not None
instance_length <= last_byte_pos)): and instance_length <= last_byte_pos)):
raise ContentRangeError( raise ContentRangeError(
'Invalid Content-Range returned: %r' % content_range) 'Invalid Content-Range returned: %r' % content_range)
if (first_byte_pos != resumed_from or if (first_byte_pos != resumed_from
(instance_length is not None and or (instance_length is not None
last_byte_pos + 1 != instance_length)): and last_byte_pos + 1 != instance_length)):
# Not what we asked for. # Not what we asked for.
raise ContentRangeError( raise ContentRangeError(
'Unexpected Content-Range returned (%r)' 'Unexpected Content-Range returned (%r)'
@ -308,9 +308,9 @@ class Downloader(object):
@property @property
def interrupted(self): def interrupted(self):
return ( return (
self.finished and self.finished
self.status.total_size and and self.status.total_size
self.status.total_size != self.status.downloaded and self.status.total_size != self.status.downloaded
) )
def chunk_downloaded(self, chunk): def chunk_downloaded(self, chunk):
@ -399,8 +399,8 @@ class ProgressReporterThread(threading.Thread):
if now - self._prev_time >= self._update_interval: if now - self._prev_time >= self._update_interval:
downloaded = self.status.downloaded downloaded = self.status.downloaded
try: try:
speed = ((downloaded - self._prev_bytes) / speed = ((downloaded - self._prev_bytes)
(now - self._prev_time)) / (now - self._prev_time))
except ZeroDivisionError: except ZeroDivisionError:
speed = 0 speed = 0
@ -434,11 +434,11 @@ class ProgressReporterThread(threading.Thread):
self._prev_bytes = downloaded self._prev_bytes = downloaded
self.output.write( self.output.write(
CLEAR_LINE + CLEAR_LINE
' ' + + ' '
SPINNER[self._spinner_pos] + + SPINNER[self._spinner_pos]
' ' + + ' '
self._status_line + self._status_line
) )
self.output.flush() self.output.flush()
@ -463,8 +463,8 @@ class ProgressReporterThread(threading.Thread):
self.output.write(SUMMARY.format( self.output.write(SUMMARY.format(
downloaded=humanize_bytes(actually_downloaded), downloaded=humanize_bytes(actually_downloaded),
total=(self.status.total_size and total=(self.status.total_size
humanize_bytes(self.status.total_size)), and humanize_bytes(self.status.total_size)),
speed=humanize_bytes(speed), speed=humanize_bytes(speed),
time=time_taken, time=time_taken,
)) ))

View File

@ -254,8 +254,8 @@ class HTTPieArgumentParser(ArgumentParser):
else: else:
credentials = parse_auth(self.args.auth) credentials = parse_auth(self.args.auth)
if (not credentials.has_password() and if (not credentials.has_password()
plugin.prompt_password): and plugin.prompt_password):
if self.args.ignore_stdin: if self.args.ignore_stdin:
# Non-tty stdin read by now # Non-tty stdin read by now
self.error( self.error(
@ -338,10 +338,11 @@ class HTTPieArgumentParser(ArgumentParser):
self.args.url = self.args.method self.args.url = self.args.method
# Infer the method # Infer the method
has_data = ( has_data = (
(not self.args.ignore_stdin and (not self.args.ignore_stdin and not self.env.stdin_isatty)
not self.env.stdin_isatty) or or any(
any(item.sep in SEP_GROUP_DATA_ITEMS item.sep in SEP_GROUP_DATA_ITEMS
for item in self.args.items) for item in self.args.items
)
) )
self.args.method = HTTP_POST if has_data else HTTP_GET self.args.method = HTTP_POST if has_data else HTTP_GET
@ -426,8 +427,8 @@ class HTTPieArgumentParser(ArgumentParser):
if self.args.prettify == PRETTY_STDOUT_TTY_ONLY: if self.args.prettify == PRETTY_STDOUT_TTY_ONLY:
self.args.prettify = PRETTY_MAP[ self.args.prettify = PRETTY_MAP[
'all' if self.env.stdout_isatty else 'none'] 'all' if self.env.stdout_isatty else 'none']
elif (self.args.prettify and self.env.is_windows and elif (self.args.prettify and self.env.is_windows
self.args.output_file): and self.args.output_file):
self.error('Only terminal output can be colorized on Windows.') self.error('Only terminal output can be colorized on Windows.')
else: else:
# noinspection PyTypeChecker # noinspection PyTypeChecker
@ -469,8 +470,8 @@ class SessionNameValidator(object):
def __call__(self, value): def __call__(self, value):
# Session name can be a path or just a name. # Session name can be a path or just a name.
if (os.path.sep not in value and if (os.path.sep not in value
not VALID_SESSION_NAME_PATTERN.search(value)): and not VALID_SESSION_NAME_PATTERN.search(value)):
raise ArgumentError(None, self.error_message) raise ArgumentError(None, self.error_message)
return value return value
@ -505,7 +506,7 @@ class KeyValueArgType(object):
"""Represents an escaped character.""" """Represents an escaped character."""
def tokenize(string): def tokenize(string):
"""Tokenize `string`. There are only two token types - strings r"""Tokenize `string`. There are only two token types - strings
and escaped characters: and escaped characters:
tokenize(r'foo\=bar\\baz') tokenize(r'foo\=bar\\baz')

View File

@ -15,8 +15,8 @@ class JSONFormatter(FormatterPlugin):
'javascript', 'javascript',
'text', 'text',
] ]
if (self.kwargs['explicit_json'] or if (self.kwargs['explicit_json']
any(token in mime for token in maybe_json)): or any(token in mime for token in maybe_json)):
try: try:
obj = json.loads(body) obj = json.loads(body)
except ValueError: except ValueError:

View File

@ -30,8 +30,8 @@ def get_response(requests_session, session_name,
if os.path.sep in session_name: if os.path.sep in session_name:
path = os.path.expanduser(session_name) path = os.path.expanduser(session_name)
else: else:
hostname = (args.headers.get('Host', None) or hostname = (args.headers.get('Host', None)
urlsplit(args.url).netloc.split('@')[-1]) or urlsplit(args.url).netloc.split('@')[-1])
if not hostname: if not hostname:
# HACK/FIXME: httpie-unixsocket's URLs have no hostname. # HACK/FIXME: httpie-unixsocket's URLs have no hostname.
hostname = 'localhost' hostname = 'localhost'

View File

@ -1,2 +0,0 @@
[pytest]
norecursedirs = tests/fixtures

View File

@ -5,3 +5,4 @@ pytest-cov
pytest-httpbin>=0.0.6 pytest-httpbin>=0.0.6
docutils docutils
wheel wheel
pycodestyle

View File

@ -1,2 +1,19 @@
[wheel] [wheel]
universal = 1 universal = 1
[tool:pytest]
# <https://docs.pytest.org/en/latest/customize.html>
norecursedirs = tests/fixtures
[pycodestyle]
# <http://pycodestyle.pycqa.org/en/latest/intro.html#configuration>
exclude = .git,.idea,__pycache__,build,dist,.tox,.pytest_cache,*.egg-info
# <http://pycodestyle.pycqa.org/en/latest/intro.html#error-codes>
# E241 - multiple spaces after ,
# E501 - line too long
# W503 - line break before binary operator
ignore = E241,E501,W503

View File

@ -49,9 +49,9 @@ class TestItemParsing:
assert 'bar@baz' in items.files assert 'bar@baz' in items.files
@pytest.mark.parametrize(('string', 'key', 'sep', 'value'), [ @pytest.mark.parametrize(('string', 'key', 'sep', 'value'), [
('path=c:\windows', 'path', '=', 'c:\windows'), ('path=c:\\windows', 'path', '=', 'c:\\windows'),
('path=c:\windows\\', 'path', '=', 'c:\windows\\'), ('path=c:\\windows\\', 'path', '=', 'c:\\windows\\'),
('path\==c:\windows', 'path=', '=', 'c:\windows'), ('path\\==c:\\windows', 'path=', '=', 'c:\\windows'),
]) ])
def test_backslash_before_non_special_character_does_not_escape( def test_backslash_before_non_special_character_does_not_escape(
self, string, key, sep, value): self, string, key, sep, value):

View File

@ -81,8 +81,8 @@ class TestSessionFlow(SessionTestBase):
assert HTTP_OK in r4 assert HTTP_OK in r4
assert r4.json['headers']['Hello'] == 'World2' assert r4.json['headers']['Hello'] == 'World2'
assert r4.json['headers']['Cookie'] == 'hello=world2' assert r4.json['headers']['Cookie'] == 'hello=world2'
assert (r2.json['headers']['Authorization'] != assert (r2.json['headers']['Authorization']
r4.json['headers']['Authorization']) != r4.json['headers']['Authorization'])
def test_session_read_only(self, httpbin): def test_session_read_only(self, httpbin):
self.start_session(httpbin) self.start_session(httpbin)
@ -157,8 +157,8 @@ class TestSession(SessionTestBase):
assert HTTP_OK in r2 assert HTTP_OK in r2
# FIXME: Authorization *sometimes* is not present on Python3 # FIXME: Authorization *sometimes* is not present on Python3
assert (r2.json['headers']['Authorization'] == assert (r2.json['headers']['Authorization']
HTTPBasicAuth.make_header(u'test', UNICODE)) == HTTPBasicAuth.make_header(u'test', UNICODE))
# httpbin doesn't interpret utf8 headers # httpbin doesn't interpret utf8 headers
assert UNICODE in r2 assert UNICODE in r2

View File

@ -119,8 +119,8 @@ class StrCLIResponse(str, BaseCLIResponse):
elif self.strip().startswith('{'): elif self.strip().startswith('{'):
# Looks like JSON body. # Looks like JSON body.
self._json = json.loads(self) self._json = json.loads(self)
elif (self.count('Content-Type:') == 1 and elif (self.count('Content-Type:') == 1
'application/json' in self): and 'application/json' in self):
# Looks like a whole JSON HTTP message, # Looks like a whole JSON HTTP message,
# try to extract its body. # try to extract its body.
try: try:

11
tox.ini
View File

@ -3,7 +3,8 @@
[tox] [tox]
envlist = py27, py35, py36, pypy, codestyle # pypy3 currently fails because of a Flask issue
envlist = py27, py37, pypy
[testenv] [testenv]
@ -20,11 +21,3 @@ commands =
--verbose \ --verbose \
--doctest-modules \ --doctest-modules \
{posargs:./httpie ./tests} {posargs:./httpie ./tests}
[testenv:codestyle]
deps = pycodestyle
commands =
pycodestyle \
--ignore=E241,E501
# 241 - multiple spaces after ,
# 501 - line too long