Skip to content

Commit

Permalink
Expand linting to include formatting and import order
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Edwards committed Nov 15, 2022
1 parent 4ffd721 commit 2b49197
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 222 deletions.
6 changes: 6 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[flake8]
application_import_names = aiodataloader
exclude = .git,.mypy_cache,.pytest_cache,.tox,.venv,__pycache__,build,dist,docs
ignore = E203
import_order_style = smarkets
max-line-length = 88
5 changes: 3 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Set up Python 3.10
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[lint]
- name: Run code quality tests
run: |
black --check .
flake8
mypy
4 changes: 1 addition & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ jobs:

strategy:
matrix:
python: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', 'pypy3.9']
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', 'pypy3.9']

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
128 changes: 75 additions & 53 deletions aiodataloader/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import sys

from asyncio import AbstractEventLoop, Future
from asyncio import gather, ensure_future, get_event_loop, iscoroutine, iscoroutinefunction
from asyncio import (
AbstractEventLoop,
ensure_future,
Future,
gather,
get_event_loop,
iscoroutine,
iscoroutinefunction,
)
from collections import namedtuple
from functools import partial

from typing import (
Any,
Callable,
Expand All @@ -24,8 +29,7 @@
else:
from typing_extensions import TypeGuard

__version__ = '0.2.1'

__version__ = "0.2.1"

KeyT = TypeVar("KeyT")
ReturnT = TypeVar("ReturnT")
Expand All @@ -40,7 +44,7 @@ def iscoroutinefunctionorpartial(
return iscoroutinefunction(fn.func if isinstance(fn, partial) else fn)


Loader = namedtuple('Loader', 'key,future')
Loader = namedtuple("Loader", "key,future")


class DataLoader(Generic[KeyT, ReturnT]):
Expand All @@ -51,7 +55,9 @@ class DataLoader(Generic[KeyT, ReturnT]):

def __init__(
self,
batch_load_fn: Optional[Callable[[List[KeyT]], Coroutine[Any, Any, List[ReturnT]]]] = None,
batch_load_fn: Optional[
Callable[[List[KeyT]], Coroutine[Any, Any, List[ReturnT]]]
] = None,
batch: Optional[bool] = None,
max_batch_size: Optional[int] = None,
cache: Optional[bool] = None,
Expand All @@ -64,14 +70,17 @@ def __init__(
if batch_load_fn is not None:
self.batch_load_fn = batch_load_fn

assert iscoroutinefunctionorpartial(self.batch_load_fn), "batch_load_fn must be coroutine. Received: {}".format(
self.batch_load_fn)
assert iscoroutinefunctionorpartial(
self.batch_load_fn
), "batch_load_fn must be coroutine. Received: {}".format(self.batch_load_fn)

if not callable(self.batch_load_fn):
raise TypeError((
'DataLoader must be have a batch_load_fn which accepts '
'Iterable<key> and returns Future<Iterable<value>>, but got: {}.'
).format(batch_load_fn))
raise TypeError(
(
"DataLoader must be have a batch_load_fn which accepts "
"Iterable<key> and returns Future<Iterable<value>>, but got: {}."
).format(batch_load_fn)
)

if batch is not None:
self.batch = batch
Expand All @@ -84,7 +93,7 @@ def __init__(

if get_cache_key is not None:
self.get_cache_key = get_cache_key
if not hasattr(self, 'get_cache_key'):
if not hasattr(self, "get_cache_key"):
self.get_cache_key = lambda x: x

self._cache = cache_map if cache_map is not None else {}
Expand All @@ -95,10 +104,12 @@ def load(self, key: Optional[KeyT] = None) -> "Future[ReturnT]":
Loads a key, returning a `Future` for the value represented by that key.
"""
if key is None:
raise TypeError((
'The loader.load() function must be called with a value, '
'but got: {}.'
).format(key))
raise TypeError(
(
"The loader.load() function must be called with a value, "
"but got: {}."
).format(key)
)

cache_key = self.get_cache_key(key)

Expand All @@ -119,10 +130,7 @@ def load(self, key: Optional[KeyT] = None) -> "Future[ReturnT]":

def do_resolve_reject(self, key: KeyT, future: "Future[ReturnT]") -> None:
# Enqueue this Future to be dispatched.
self._queue.append(Loader(
key=key,
future=future
))
self._queue.append(Loader(key=key, future=future))
# Determine if a dispatch of this queue should be scheduled.
# A single dispatch should be scheduled per queue at the time when the
# queue changes from "empty" to "full".
Expand All @@ -148,10 +156,12 @@ def load_many(self, keys: Iterable[KeyT]) -> "Future[List[ReturnT]]":
>>> )
"""
if not isinstance(keys, Iterable):
raise TypeError((
'The loader.load_many() function must be called with Iterable<key> '
'but got: {}.'
).format(keys))
raise TypeError(
(
"The loader.load_many() function must be called with Iterable<key> "
"but got: {}."
).format(keys)
)

return gather(*[self.load(key) for key in keys])

Expand Down Expand Up @@ -195,7 +205,9 @@ def prime(self: DataLoaderT, key: KeyT, value: ReturnT) -> DataLoaderT:
return self


def enqueue_post_future_job(loop: AbstractEventLoop, loader: DataLoader[Any, Any]) -> None:
def enqueue_post_future_job(
loop: AbstractEventLoop, loader: DataLoader[Any, Any]
) -> None:
async def dispatch() -> None:
dispatch_queue(loader)

Expand All @@ -204,7 +216,10 @@ async def dispatch() -> None:

def get_chunks(iterable_obj: List[T], chunk_size: int = 1) -> Iterator[List[T]]:
chunk_size = max(1, chunk_size)
return (iterable_obj[i:i + chunk_size] for i in range(0, len(iterable_obj), chunk_size))
return (
iterable_obj[i : i + chunk_size]
for i in range(0, len(iterable_obj), chunk_size)
)


def dispatch_queue(loader: DataLoader[Any, Any]) -> None:
Expand All @@ -223,15 +238,14 @@ def dispatch_queue(loader: DataLoader[Any, Any]) -> None:
if max_batch_size and max_batch_size < len(queue):
chunks = get_chunks(queue, max_batch_size)
for chunk in chunks:
ensure_future(dispatch_queue_batch(
loader,
chunk
))
ensure_future(dispatch_queue_batch(loader, chunk))
else:
ensure_future(dispatch_queue_batch(loader, queue))


async def dispatch_queue_batch(loader: DataLoader[Any, Any], queue: List[Loader]) -> None:
async def dispatch_queue_batch(
loader: DataLoader[Any, Any], queue: List[Loader]
) -> None:
# Collect all keys to be loaded in this dispatch
keys = [ql.key for ql in queue]

Expand All @@ -243,32 +257,38 @@ async def dispatch_queue_batch(loader: DataLoader[Any, Any], queue: List[Loader]
return failed_dispatch(
loader,
queue,
TypeError((
'DataLoader must be constructed with a function which accepts '
'Iterable<key> and returns Future<Iterable<value>>, but the function did '
'not return a Coroutine: {}.'
).format(batch_future))
TypeError(
(
"DataLoader must be constructed with a function which accepts "
"Iterable<key> and returns Future<Iterable<value>>, but the "
"function did not return a Coroutine: {}."
).format(batch_future)
),
)

try:
values = await batch_future
if not isinstance(values, Iterable):
raise TypeError((
'DataLoader must be constructed with a function which accepts '
'Iterable<key> and returns Future<Iterable<value>>, but the function did '
'not return a Future of a Iterable: {}.'
).format(values))
raise TypeError(
(
"DataLoader must be constructed with a function which accepts "
"Iterable<key> and returns Future<Iterable<value>>, but the "
"function did not return a Future of a Iterable: {}."
).format(values)
)

values = list(values)
if len(values) != len(keys):
raise TypeError((
'DataLoader must be constructed with a function which accepts '
'Iterable<key> and returns Future<Iterable<value>>, but the function did '
'not return a Future of a Iterable with the same length as the Iterable '
'of keys.'
'\n\nKeys:\n{}'
'\n\nValues:\n{}'
).format(keys, values))
raise TypeError(
(
"DataLoader must be constructed with a function which accepts "
"Iterable<key> and returns Future<Iterable<value>>, but the "
"function did not return a Future of a Iterable with the same "
"length as the Iterable of keys."
"\n\nKeys:\n{}"
"\n\nValues:\n{}"
).format(keys, values)
)

# Step through the values, resolving or rejecting each Future in the
# loaded queue.
Expand All @@ -282,7 +302,9 @@ async def dispatch_queue_batch(loader: DataLoader[Any, Any], queue: List[Loader]
return failed_dispatch(loader, queue, e)


def failed_dispatch(loader: DataLoader[Any, Any], queue: List[Loader], error: Exception) -> None:
def failed_dispatch(
loader: DataLoader[Any, Any], queue: List[Loader], error: Exception
) -> None:
"""
Do not cache individual loads if the entire batch dispatch fails,
but still reject each request so they do not hang.
Expand Down
14 changes: 14 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[tool.black]
target-version = ["py36", "py37", "py38", "py39", "py310", "py311"]

[tool.isort]
force_single_line = false
known_first_party = "aiodataloader"
order_by_type = false
profile = "black"

[tool.mypy]
files = ["aiodataloader/**/*.py", "setup.py", "test_aiodataloader.py"]
follow_imports = "silent"
ignore_missing_imports = true
strict = true
9 changes: 0 additions & 9 deletions setup.cfg

This file was deleted.

65 changes: 31 additions & 34 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,54 @@
import os
import sys
from setuptools import setup, find_packages
from setuptools import setup


def get_version(filename):
def get_version(filename: str) -> str:
import os
import re

here = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(here, filename)) as f:
version_file = f.read()
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
version_file, re.M)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")


version = get_version('aiodataloader/__init__.py')
version = get_version("aiodataloader/__init__.py")

tests_require = [
'pytest>=3.6', 'pytest-cov', 'coveralls', 'mock', 'pytest-asyncio'
]
tests_require = ["pytest>=3.6", "pytest-cov", "coveralls", "mock", "pytest-asyncio"]

setup(
name='aiodataloader',
name="aiodataloader",
version=version,
description='Asyncio DataLoader implementation for Python',
long_description=open('README.md').read(),
description="Asyncio DataLoader implementation for Python",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url='https://github.com/syrusakbary/aiodataloader',
download_url='https://github.com/syrusakbary/aiodataloader/releases',
author='Syrus Akbary',
author_email='me@syrusakbary.com',
license='MIT',
url="https://github.com/syrusakbary/aiodataloader",
download_url="https://github.com/syrusakbary/aiodataloader/releases",
author="Syrus Akbary",
author_email="me@syrusakbary.com",
license="MIT",
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'License :: OSI Approved :: MIT License',
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: MIT License",
],
keywords='concurrent future deferred aiodataloader',
py_modules=['aiodataloader'],
python_requires='>=3.6',
keywords="concurrent future deferred aiodataloader",
py_modules=["aiodataloader"],
python_requires=">=3.6",
extras_require={
'lint': ['flake8', 'mypy'],
'test': tests_require,
"lint": ["black", "flake8", "flake8-import-order", "mypy"],
"test": tests_require,
},
install_requires=["typing_extensions>=4.3.0"],
tests_require=tests_require, )
tests_require=tests_require,
)

0 comments on commit 2b49197

Please sign in to comment.