diff --git a/.flake8 b/.flake8 index 3997777..ffb0b3b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -ignore = C813 +ignore = C814 import-order-style = google application-import-names = flask_annex diff --git a/.gitignore b/.gitignore index f2a536c..ba74660 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,3 @@ docs/_build/ # PyBuilder target/ - -# Converted README for PyPI -README.rst diff --git a/.travis.yml b/.travis.yml index f4a21f5..d5425b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,26 +1,20 @@ -sudo: false +dist: xenial language: python python: + - "3.7" - "3.6" - - "pypy" env: - TOXENV=py-s3 matrix: include: - - python: "3.6" - env: TOXENV=py-base - - # TODO: Remove this workaround once travis-ci/travis-ci#9815 is fixed. - sudo: true - dist: xenial - python: "3.7" - env: TOXENV=py-s3 - -cache: - directories: - - $HOME/.cache/pip + - env: TOXENV=lint + - env: TOXENV=py-base + - python: pypy3 + +cache: pip before_install: - pip install -U pip diff --git a/flask_annex/__init__.py b/flask_annex/__init__.py index 63dbd5e..2d6b698 100644 --- a/flask_annex/__init__.py +++ b/flask_annex/__init__.py @@ -11,7 +11,7 @@ def get_annex_class(storage): from .s3 import S3Annex return S3Annex else: - raise ValueError("unsupported storage {}".format(storage)) + raise ValueError(f"unsupported storage {storage}") # ----------------------------------------------------------------------------- @@ -19,7 +19,7 @@ def get_annex_class(storage): # We don't use the base class here, as this is just a convenience thing rather # than an actual annex class. -class Annex(object): +class Annex: def __new__(cls, storage, *args, **kwargs): annex_class = get_annex_class(storage) return annex_class(*args, **kwargs) @@ -30,7 +30,7 @@ def from_env(namespace): # Use storage-specific env namespace when configuring a generic annex, # to avoid having unrecognized extra keys when changing storage. - storage_namespace = '{}_{}'.format(namespace, storage.upper()) + storage_namespace = f'{namespace}_{storage.upper()}' annex_class = get_annex_class(storage) return annex_class.from_env(storage_namespace) diff --git a/flask_annex/base.py b/flask_annex/base.py index 85c96cc..1ec6fd7 100644 --- a/flask_annex/base.py +++ b/flask_annex/base.py @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- -class AnnexBase(object): +class AnnexBase: @classmethod def from_env(cls, namespace): return cls(**utils.get_config_from_env(namespace)) diff --git a/flask_annex/compat.py b/flask_annex/compat.py deleted file mode 100644 index ef8d3c2..0000000 --- a/flask_annex/compat.py +++ /dev/null @@ -1,28 +0,0 @@ -import fnmatch -import os -import sys - -# ----------------------------------------------------------------------------- - -PY2 = sys.version_info[0] == 2 - -# ----------------------------------------------------------------------------- - -if PY2: - string_types = (str, unicode) # noqa: F821 -else: - string_types = (str,) - -# ----------------------------------------------------------------------------- - - -def recursive_glob(root_dir, pattern='*'): - """Search recursively for files matching a specified pattern. - - Adapted from http://stackoverflow.com/questions/2186525/use-a-glob-to-find-files-recursively-in-python # noqa: E501 - """ - return tuple( - os.path.join(root, filename) - for root, _dirnames, filenames in os.walk(root_dir) - for filename in fnmatch.filter(filenames, pattern) - ) diff --git a/flask_annex/file.py b/flask_annex/file.py index 046fd88..514415e 100644 --- a/flask_annex/file.py +++ b/flask_annex/file.py @@ -5,7 +5,6 @@ import flask from .base import AnnexBase -from .compat import recursive_glob, string_types # ----------------------------------------------------------------------------- @@ -20,10 +19,8 @@ def _get_filename(self, key): def delete(self, key): try: os.unlink(self._get_filename(key)) - except OSError as e: - if e.errno != errno.ENOENT: - # It's fine if the file doesn't exist. - raise # pragma: no cover + except FileNotFoundError: + pass self._clean_empty_dirs(key) @@ -34,11 +31,13 @@ def _clean_empty_dirs(self, key): dir_name = self._get_filename(key_dir_name) try: os.rmdir(dir_name) + except FileNotFoundError: + pass except OSError as e: if e.errno == errno.ENOTEMPTY: break - if e.errno != errno.ENOENT: - raise # pragma: no cover + + raise # pragma: no cover key_dir_name = os.path.dirname(key_dir_name) @@ -49,7 +48,7 @@ def delete_many(self, keys): def get_file(self, key, out_file): in_filename = self._get_filename(key) - if isinstance(out_file, string_types): + if isinstance(out_file, str): shutil.copyfile(in_filename, out_file) else: with open(in_filename, 'rb') as in_fp: @@ -57,7 +56,15 @@ def get_file(self, key, out_file): def list_keys(self, prefix): root = self._get_filename(prefix) - filenames = (root,) if os.path.isfile(root) else recursive_glob(root) + filenames = ( + (root,) + if os.path.isfile(root) + else tuple( + os.path.join(root, filename) + for root, _dirnames, filenames in os.walk(root) + for filename in filenames + ) + ) return tuple( os.path.relpath(filename, self._root_path) @@ -68,7 +75,7 @@ def save_file(self, key, in_file): out_filename = self._get_filename(key) self._ensure_key_dir(key) - if isinstance(in_file, string_types): + if isinstance(in_file, str): shutil.copyfile(in_file, out_filename) else: with open(out_filename, 'wb') as out_fp: @@ -81,15 +88,11 @@ def _ensure_key_dir(self, key): # Verify that we aren't trying to create the root path. if not os.path.exists(self._root_path): - raise IOError( - "root path {} does not exist".format(self._root_path), + raise FileNotFoundError( + f"root path {self._root_path} does not exist", ) - try: - os.makedirs(dir_name) - except OSError as e: # pragma: no cover - if e.errno != errno.EEXIST: - raise + os.makedirs(dir_name, exist_ok=True) def send_file(self, key): return flask.send_from_directory( diff --git a/flask_annex/s3.py b/flask_annex/s3.py index 9934c2a..fdc28fe 100644 --- a/flask_annex/s3.py +++ b/flask_annex/s3.py @@ -4,7 +4,6 @@ import flask from .base import AnnexBase -from .compat import string_types # ----------------------------------------------------------------------------- @@ -19,6 +18,7 @@ class S3Annex(AnnexBase): def __init__( self, bucket_name, + *, region=None, access_key_id=None, secret_access_key=None, @@ -43,8 +43,7 @@ def delete_many(self, keys): # casting to tuple because keys might be iterable objects = tuple({'Key': key} for key in keys) if not objects: - # this is not just an optimization, boto fails if the array - # is empty + # boto fails if the array is empty. return self._client.delete_objects( @@ -53,7 +52,7 @@ def delete_many(self, keys): ) def get_file(self, key, out_file): - if isinstance(out_file, string_types): + if isinstance(out_file, str): self._client.download_file(self._bucket_name, key, out_file) else: self._client.download_fileobj(self._bucket_name, key, out_file) @@ -77,7 +76,7 @@ def save_file(self, key, in_file): else: extra_args = None - if isinstance(in_file, string_types): + if isinstance(in_file, str): self._client.upload_file( in_file, self._bucket_name, key, extra_args, ) diff --git a/flask_annex/utils.py b/flask_annex/utils.py index 25631a3..356ce6b 100644 --- a/flask_annex/utils.py +++ b/flask_annex/utils.py @@ -4,7 +4,7 @@ def get_config_from_env(namespace): - prefix = '{}_'.format(namespace) + prefix = f'{namespace}_' return { key[len(prefix):].lower(): value diff --git a/setup.cfg b/setup.cfg index d8673a6..bd6a4ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,4 +2,5 @@ universal = 1 [metadata] -long_description = file: README.rst +long_description = file: README.md +long_description_content_type = text/markdown diff --git a/setup.py b/setup.py index 41d351f..ae46108 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,8 @@ def run(self): author="Jimmy Jia", author_email='tesrin@gmail.com', license='MIT', - classifiers=( + python_requires=">=3.6", + classifiers=[ 'Development Status :: 2 - Pre-Alpha', 'Framework :: Flask', 'Environment :: Web Environment', @@ -40,9 +41,12 @@ def run(self): 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3 :: Only', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules', - ), + ], keywords='storage s3 flask', packages=('flask_annex',), install_requires=( @@ -50,17 +54,13 @@ def run(self): ), extras_require={ 's3': ('boto3 >= 1.4.0',), - 'tests': ( - 'mock', - 'moto', - 'pytest', - 'requests', - ), + 'lint': ('flake8', 'flake8-config-4catalyzer'), + 'tests': ('pytest', 'pytest-cov'), + 'tests-s3': ('moto', 'requests'), }, cmdclass={ 'clean': system('rm -rf build dist *.egg-info'), - 'package': system('python setup.py pandoc sdist bdist_wheel'), - 'pandoc': system('pandoc README.md -o README.rst'), + 'package': system('python setup.py sdist bdist_wheel'), 'publish': system('twine upload dist/*'), 'release': system('python setup.py clean package publish'), 'test': system('tox'), diff --git a/tests/helpers.py b/tests/helpers.py index 2bf743c..8a5fd3e 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -16,14 +16,14 @@ def assert_key_value(annex, key, value): def get_upload_info(client, key, **kwargs): - response = client.get('/upload_info/{}'.format(key), **kwargs) + response = client.get(f'/upload_info/{key}', **kwargs) return json.loads(response.get_data(as_text=True)) # ----------------------------------------------------------------------------- -class AbstractTestAnnex(object): +class AbstractTestAnnex: @pytest.fixture def annex(self, annex_base): annex_base.save_file('foo/bar.txt', BytesIO(b'1\n')) diff --git a/tests/test_s3.py b/tests/test_s3.py index c985fd0..07b1139 100644 --- a/tests/test_s3.py +++ b/tests/test_s3.py @@ -1,8 +1,8 @@ import base64 from io import BytesIO import json +from unittest.mock import Mock -from mock import Mock import pytest from flask_annex import Annex diff --git a/tox.ini b/tox.ini index cc54f9a..5beb3ed 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,18 @@ [tox] -envlist = py{36,37}-{base,s3} +envlist = + lint + py{36,37}-{base,s3} [testenv] usedevelop = True - deps = - flake8 - flake8-config-4catalyzer - mock - pytest - pytest-cov - s3: moto s3: requests - extras = - s3: s3 + tests + s3: s3, tests-s3 +commands = pytest --cov {posargs} -commands = - flake8 . - pytest --cov +[testenv:lint] +extras = lint +commands = flake8 .