From 598015702dadd83c62b5c48dab355aa98c041693 Mon Sep 17 00:00:00 2001 From: draos Date: Tue, 21 Feb 2023 13:55:57 +0100 Subject: [PATCH 1/2] Convert SearchSet to the required frozenset --- pymap/parsing/specials/searchkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymap/parsing/specials/searchkey.py b/pymap/parsing/specials/searchkey.py index b06295da..a360c989 100644 --- a/pymap/parsing/specials/searchkey.py +++ b/pymap/parsing/specials/searchkey.py @@ -186,7 +186,7 @@ def parse(cls, buf: memoryview, params: Params) \ pass else: key_list = key_list_p.get_as(SearchKey) - return cls(b'KEYSET', key_list, inverse), buf + return cls(b'KEYSET', frozenset(key_list), inverse), buf atom, after = Atom.parse(buf, params) key = atom.value.upper() if key in (b'ALL', b'ANSWERED', b'DELETED', b'FLAGGED', b'NEW', b'OLD', From 6f197aa3552166a22eeb321fcea7ddee55e0dcb1 Mon Sep 17 00:00:00 2001 From: Ian Good Date: Sat, 18 Mar 2023 19:25:58 -0400 Subject: [PATCH 2/2] Switch to hatch and importlib --- .flake8 | 4 + .github/workflows/python-package.yml | 4 +- .github/workflows/python-publish.yml | 6 +- LICENSE.md | 2 +- MANIFEST.in | 5 -- doc/source/conf.py | 4 +- docker/Dockerfile | 2 +- pymap/__init__.py | 4 +- pymap/backend/dict/__init__.py | 25 +++--- pymap/backend/maildir/mailbox.py | 6 +- pymap/backend/redis/__init__.py | 7 +- pymap/backend/redis/scripts/__init__.py | 6 +- pymap/flags.py | 2 +- pymap/main.py | 8 +- pymap/message.py | 2 +- pymap/parsing/primitives.py | 11 +-- pymap/parsing/specials/searchkey.py | 2 +- pymap/plugin.py | 16 ++-- pyproject.toml | 92 +++++++++++++++++++- requirements-dev.txt | 4 +- setup.py | 83 ------------------ tasks/__init__.py | 5 -- tasks/lint.py | 2 +- test/server/base.py | 29 ++++--- test/server/mocktransport.py | 108 ++++++++++++++++++------ test/server/test_admin_auth.py | 24 +++--- test/server/test_admin_mailbox.py | 43 +++++++--- test/server/test_admin_system.py | 9 +- test/server/test_admin_user.py | 14 +-- test/server/test_copy.py | 20 +++-- test/server/test_expunge.py | 9 +- test/server/test_fetch.py | 34 +++++--- test/server/test_idle.py | 14 +-- test/server/test_mailbox.py | 35 ++++---- test/server/test_managesieve.py | 45 ++++++---- test/server/test_readonly.py | 16 ++-- test/server/test_rename.py | 18 ++-- test/server/test_search.py | 44 +++++----- test/server/test_session.py | 13 +-- test/server/test_store.py | 16 ++-- test/test_parsing_command_select.py | 4 +- test/test_parsing_primitives.py | 2 +- test/test_parsing_specials.py | 3 +- 43 files changed, 465 insertions(+), 337 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 setup.py diff --git a/.flake8 b/.flake8 index d0e68d35..0eff6d67 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,6 @@ [flake8] extend-select = B901, B902, B903, B904 +extend-ignore = ANN101, ANN102 +per-file-ignores = + test/*: ANN + tasks/*: ANN, B028 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2d430ec7..b8ad7cfb 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -23,7 +23,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install build tools run: | - python -m pip install --upgrade pip setuptools wheel invoke coveralls + python -m pip install --upgrade pip invoke coveralls - name: Install package and dependencies run: | invoke install @@ -49,7 +49,7 @@ jobs: python-version: '3.11' - name: Install build tools run: | - python -m pip install --upgrade pip setuptools wheel invoke + python -m pip install --upgrade pip invoke - name: Build the Sphinx documentation run: | invoke install doc.install doc.build diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 3f9bd680..68373bb3 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -17,13 +17,13 @@ jobs: python-version: '3.11' - name: Install build tools run: | - python -m pip install --upgrade pip setuptools wheel twine + python -m pip install --upgrade pip build twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel + python -m build twine upload dist/* docs: @@ -38,7 +38,7 @@ jobs: python-version: '3.11' - name: Install build tools run: | - python -m pip install --upgrade pip setuptools wheel invoke + python -m pip install --upgrade pip invoke - name: Build the Sphinx documentation run: | invoke install doc.install doc.build diff --git a/LICENSE.md b/LICENSE.md index 2daab038..73f73f7f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ ## The MIT License (MIT) -Copyright (c) 2022 Ian Good +Copyright (c) 2023 Ian Good Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b7142a2e..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include README.md LICENSE.md pymap/py.typed -recursive-include pymap *.pyi -recursive-include pymap/backend/dict/demo/ * -recursive-include pymap/backend/redis/scripts/lua/ *.lua -prune tasks diff --git a/doc/source/conf.py b/doc/source/conf.py index ba0501f9..70413229 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -14,7 +14,7 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) -import pkg_resources +from importlib.metadata import distribution import cloud_sptheme as csp # type: ignore @@ -26,7 +26,7 @@ author = 'Ian Good' # The short X.Y version -project_version = pkg_resources.require(project)[0].version +project_version = distribution(project).version version_parts = project_version.split('.') version = '.'.join(version_parts[0:2]) # The full version, including alpha/beta/rc tags diff --git a/docker/Dockerfile b/docker/Dockerfile index 098a2afe..454b1f91 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.11-alpine WORKDIR /pymap COPY . . -RUN pip install -U pip wheel setuptools typing-extensions +RUN pip install -U pip typing-extensions RUN apk --update add --virtual build-dependencies \ build-base python3-dev libffi-dev \ diff --git a/pymap/__init__.py b/pymap/__init__.py index 07d25865..0b1989fd 100644 --- a/pymap/__init__.py +++ b/pymap/__init__.py @@ -5,9 +5,9 @@ """ -import pkg_resources +from importlib.metadata import distribution __all__ = ['__version__'] #: The package version string. -__version__: str = pkg_resources.require(__package__)[0].version +__version__: str = distribution(__package__).version diff --git a/pymap/backend/dict/__init__.py b/pymap/backend/dict/__init__.py index 0304d4fa..edecbbe6 100644 --- a/pymap/backend/dict/__init__.py +++ b/pymap/backend/dict/__init__.py @@ -8,10 +8,10 @@ from collections.abc import Set, Mapping, AsyncIterator from contextlib import closing, asynccontextmanager, AsyncExitStack from datetime import datetime, timezone +from importlib.resources import files from secrets import token_bytes from typing import Any, Final -from pkg_resources import resource_listdir, resource_stream from pysasl.creds.server import ServerCredentials from pymap.config import BackendCapability, IMAPConfig @@ -243,27 +243,29 @@ async def _load_demo(self, resource: str, mailbox_set: MailboxSet, filter_set: FilterSet) -> None: inbox = await mailbox_set.get_mailbox('INBOX') await self._load_demo_mailbox(resource, 'INBOX', inbox) - mbx_names = sorted(resource_listdir(resource, 'demo')) + mbx_names = sorted(f.name + for f in files(resource).joinpath('demo').iterdir() + if f.is_dir()) + await self._load_demo_sieve(resource, filter_set) for name in mbx_names: - if name == 'sieve': - await self._load_demo_sieve(resource, name, filter_set) - elif name != 'INBOX': + if name != 'INBOX': await mailbox_set.add_mailbox(name) mbx = await mailbox_set.get_mailbox(name) await self._load_demo_mailbox(resource, name, mbx) - async def _load_demo_sieve(self, resource: str, name: str, + async def _load_demo_sieve(self, resource: str, filter_set: FilterSet) -> None: - path = os.path.join('demo', name) - with closing(resource_stream(resource, path)) as sieve_stream: - sieve = sieve_stream.read() + path = os.path.join('demo', 'sieve') + sieve = files(resource).joinpath(path).read_bytes() await filter_set.put('demo', sieve) await filter_set.set_active('demo') async def _load_demo_mailbox(self, resource: str, name: str, mbx: MailboxData) -> None: path = os.path.join('demo', name) - msg_names = sorted(resource_listdir(resource, path)) + msg_names = sorted(f.name + for f in files(resource).joinpath(path).iterdir() + if f.is_file()) for msg_name in msg_names: if msg_name == '.readonly': mbx._readonly = True @@ -271,7 +273,8 @@ async def _load_demo_mailbox(self, resource: str, name: str, elif msg_name.startswith('.'): continue msg_path = os.path.join(path, msg_name) - with closing(resource_stream(resource, msg_path)) as msg_stream: + with closing(files(resource).joinpath(msg_path).open('rb')) \ + as msg_stream: flags_line = msg_stream.readline() msg_timestamp = float(msg_stream.readline()) msg_data = msg_stream.read() diff --git a/pymap/backend/maildir/mailbox.py b/pymap/backend/maildir/mailbox.py index 53fc4754..4da4b302 100644 --- a/pymap/backend/maildir/mailbox.py +++ b/pymap/backend/maildir/mailbox.py @@ -7,7 +7,7 @@ from collections.abc import Iterable, AsyncIterable from datetime import datetime from mailbox import Maildir as _Maildir, MaildirMessage -from typing import Final, Any, Literal +from typing import Any, Final, Literal, Self from pymap.concurrent import Event, ReadWriteLock from pymap.context import subsystem @@ -44,7 +44,7 @@ def _path_cur(self) -> str: return self._paths['cur'] # type: ignore def _join(self, subpath: str) -> str: - base_path: str = self._path # type: ignore + base_path: str = self._path return os.path.join(base_path, subpath) def _split(self, subpath: str) \ @@ -170,7 +170,7 @@ def from_maildir(cls, uid: int, maildir_msg: MaildirMessage, maildir: Maildir, key: str, email_id: ObjectId | None, thread_id: ObjectId | None, - maildir_flags: MaildirFlags) -> Message: + maildir_flags: MaildirFlags) -> Self: flag_set = maildir_flags.from_maildir(maildir_msg.get_flags()) recent = maildir_msg.get_subdir() == 'new' msg_dt = datetime.fromtimestamp(maildir_msg.get_date()) diff --git a/pymap/backend/redis/__init__.py b/pymap/backend/redis/__init__.py index 59ed04db..66148c5a 100644 --- a/pymap/backend/redis/__init__.py +++ b/pymap/backend/redis/__init__.py @@ -12,7 +12,7 @@ from contextlib import asynccontextmanager, suppress, AsyncExitStack from datetime import datetime from secrets import token_bytes -from typing import TypeAlias, Any, Final +from typing import Any, Final, Self, TypeAlias from redis.asyncio import Redis from pysasl.creds.server import ServerCredentials @@ -88,7 +88,7 @@ def add_subparser(cls, name: str, subparsers: Any) -> ArgumentParser: @classmethod async def init(cls, args: Namespace, **overrides: Any) \ - -> tuple[RedisBackend, Config]: + -> tuple[Self, Config]: config = Config.from_args(args) status = HealthStatus(name='redis') login = Login(config, status) @@ -408,7 +408,8 @@ class User(UserMetadata): @classmethod def _from_dict(cls, config: IMAPConfig, authcid: str, - data: Mapping[str, str]) -> User: + data: Mapping[str, str]) -> Self: + params: Mapping[str, str] match data: case {'password': password, **params}: return cls(config, authcid, password=password, params=params) diff --git a/pymap/backend/redis/scripts/__init__.py b/pymap/backend/redis/scripts/__init__.py index 5967d24c..299ddca6 100644 --- a/pymap/backend/redis/scripts/__init__.py +++ b/pymap/backend/redis/scripts/__init__.py @@ -4,13 +4,12 @@ import hashlib import os.path from collections.abc import Sequence -from contextlib import closing +from importlib.resources import files from typing import final, Generic, TypeAlias, TypeVar, Any import msgpack from redis.asyncio import Redis from redis.exceptions import NoScriptError -from pkg_resources import resource_stream __all__ = ['ScriptBase'] @@ -35,8 +34,7 @@ def __init__(self, name: str) -> None: def _load(self) -> tuple[str, bytes]: fname = os.path.join('lua', f'{self._name}.lua') - with closing(resource_stream(__name__, fname)) as script: - data = script.read() + data = files(__name__).joinpath(fname).read_bytes() # Redis docs specify using SHA1 here: return hashlib.sha1(data).hexdigest(), data # nosec diff --git a/pymap/flags.py b/pymap/flags.py index c2c961fa..dae74279 100644 --- a/pymap/flags.py +++ b/pymap/flags.py @@ -107,7 +107,7 @@ class SessionFlags: __slots__ = ['_defined', '_flags', '_recent'] - def __init__(self, defined: Iterable[Flag]): + def __init__(self, defined: Iterable[Flag]) -> None: super().__init__() self._defined = frozenset(defined) - _recent_set self._flags: dict[int, frozenset[Flag]] = {} diff --git a/pymap/main.py b/pymap/main.py index f21e77eb..3c00e9ce 100644 --- a/pymap/main.py +++ b/pymap/main.py @@ -36,9 +36,11 @@ def PidFile(*args: Any, **kwargs: Any) -> Any: return nullcontext() try: - import passlib + __import__('passlib') except ImportError: - passlib = None + has_passlib = False +else: + has_passlib = True def main() -> None: @@ -57,7 +59,7 @@ def main() -> None: help='config file for logging') parser.add_argument('--no-service', dest='skip_services', action='append', metavar='NAME', help='do not run the given service') - if passlib is not None: + if has_passlib: parser.add_argument('--passlib-cfg', metavar='PATH', help='config file for passlib hashing') subparsers = parser.add_subparsers(dest='backend', required=True, diff --git a/pymap/message.py b/pymap/message.py index d5250ad1..5047e1d4 100644 --- a/pymap/message.py +++ b/pymap/message.py @@ -101,7 +101,7 @@ class ExpungedMessage(BaseMessage): """ - def __init__(self, cached_msg: CachedMessage): + def __init__(self, cached_msg: CachedMessage) -> None: super().__init__(cached_msg.uid, cached_msg.internal_date, cached_msg.permanent_flags, email_id=cached_msg.email_id, diff --git a/pymap/parsing/primitives.py b/pymap/parsing/primitives.py index 3174ab5e..b1ac764a 100644 --- a/pymap/parsing/primitives.py +++ b/pymap/parsing/primitives.py @@ -400,7 +400,7 @@ def __bytes__(self) -> bytes: return self._raw -class List(Parseable[Sequence[MaybeBytes]]): +class List(Parseable[tuple[MaybeBytes, ...]]): """Represents a list of :class:`Parseable` objects from an IMAP stream. Args: @@ -417,19 +417,16 @@ def __init__(self, items: Iterable[MaybeBytes], sort: bool = False) -> None: super().__init__() if sort: - items_list = sorted(items) # type: ignore - else: - items_list = list(items) - self.items: Sequence[MaybeBytes] = items_list + items = tuple(sorted(items)) # type: ignore + self.items: tuple[MaybeBytes, ...] = tuple(items) @property - def value(self) -> Sequence[MaybeBytes]: + def value(self) -> tuple[MaybeBytes, ...]: """The list of parsed objects.""" return self.items def get_as(self, cls: type[MaybeBytesT]) -> Sequence[MaybeBytesT]: """Return the list of parsed objects.""" - _ = cls # noqa return cast(Sequence[MaybeBytesT], self.items) def __iter__(self) -> Iterator[MaybeBytes]: diff --git a/pymap/parsing/specials/searchkey.py b/pymap/parsing/specials/searchkey.py index a360c989..b06295da 100644 --- a/pymap/parsing/specials/searchkey.py +++ b/pymap/parsing/specials/searchkey.py @@ -186,7 +186,7 @@ def parse(cls, buf: memoryview, params: Params) \ pass else: key_list = key_list_p.get_as(SearchKey) - return cls(b'KEYSET', frozenset(key_list), inverse), buf + return cls(b'KEYSET', key_list, inverse), buf atom, after = Atom.parse(buf, params) key = atom.value.upper() if key in (b'ALL', b'ANSWERED', b'DELETED', b'FLAGGED', b'NEW', b'OLD', diff --git a/pymap/plugin.py b/pymap/plugin.py index f84eaadb..fae27358 100644 --- a/pymap/plugin.py +++ b/pymap/plugin.py @@ -2,10 +2,9 @@ from __future__ import annotations from collections.abc import Callable, Iterable, Iterator, Mapping +from importlib.metadata import entry_points from typing import TypeVar, Generic, Final -from pkg_resources import iter_entry_points, DistributionNotFound - __all__ = ['PluginT', 'Plugin'] #: The plugin type variable. @@ -13,7 +12,8 @@ class Plugin(Generic[PluginT], Iterable[tuple[str, type[PluginT]]]): - """Plugin system, typically loaded from :mod:`pkg_resources` `entry points + """Plugin system, typically loaded from :mod:`importlib.metadata` + `entry points `_. >>> example: Plugin[Example] = Plugin('plugins.example') @@ -79,13 +79,9 @@ def _load(self) -> Mapping[str, type[PluginT]]: loaded = self._loaded if loaded is None: loaded = {} - for entry_point in iter_entry_points(self.group): - try: - plugin: type[PluginT] = entry_point.load() - except DistributionNotFound: - pass # optional dependencies not installed - else: - loaded[entry_point.name] = plugin + for entry_point in entry_points(group=self.group): + plugin: type[PluginT] = entry_point.load() + loaded[entry_point.name] = plugin self._loaded = loaded return loaded diff --git a/pyproject.toml b/pyproject.toml index fe215e76..7c9196fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,92 @@ +# Copyright (c) 2023 Ian C. Good +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + [build-system] -requires = ['setuptools', 'wheel'] +requires = ['hatchling'] +build-backend = 'hatchling.build' + +[project] +name = 'pymap' +version = '0.29.0.rc1' +authors = [ + { name = 'Ian Good', email = 'ian@icgood.net' }, +] +description = 'Lightweight, asynchronous IMAP serving in Python.' +license = { file = 'LICENSE.md' } +readme = { file = 'README.md', content-type = 'text/markdown' } +requires-python = '~=3.11' +classifiers = [ + 'Development Status :: 3 - Alpha', + 'Topic :: Communications :: Email :: Post-Office', + 'Topic :: Communications :: Email :: Post-Office :: IMAP', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.11', +] +dependencies = [ + 'pysasl ~= 1.0', + 'proxy-protocol ~= 0.9.1', +] + +[project.optional-dependencies] +admin = ['pymap-admin ~= 0.9.0', 'protobuf', 'googleapis-common-protos'] +macaroon = ['pymacaroons'] +redis = ['redis ~= 4.2', 'msgpack ~= 1.0'] +sieve = ['sievelib'] +swim = ['swim-protocol ~= 0.3.10'] +systemd = ['systemd-python'] +optional = ['hiredis', 'passlib', 'pid'] + +[project.urls] +homepage = 'https://github.com/icgood/pymap/' + +[project.scripts] +pymap = 'pymap.main:main' + +[project.entry-points.'pymap.backend'] +dict = 'pymap.backend.dict:DictBackend' +maildir = 'pymap.backend.maildir:MaildirBackend' +redis = 'pymap.backend.redis:RedisBackend [redis]' + +[project.entry-points.'pymap.service'] +imap = 'pymap.imap:IMAPService' +admin = 'pymap.admin:AdminService [admin]' +managesieve = 'pymap.sieve.manage:ManageSieveService [sieve]' +swim = 'pymap.cluster.swim:SwimService [swim]' + +[project.entry-points.'pymap.filter'] +sieve = 'pymap.sieve:SieveCompiler [sieve]' + +[project.entry-points.'pymap.token'] +macaroon = 'pymap.token.macaroon:MacaroonTokens [macaroon]' + +[project.entry-points.'pymap.admin.handlers'] +server = 'pymap.admin.handlers.system:SystemHandlers' +mailbox = 'pymap.admin.handlers.mailbox:MailboxHandlers' +user = 'pymap.admin.handlers.user:UserHandlers' + +[tool.hatch.build] +exclude = ['/tasks', '/doc', '/.github'] [tool.mypy] files = ['pymap', 'test'] @@ -40,9 +127,6 @@ ignore_missing_imports = true [[tool.mypy.overrides]] module = 'pid.*' ignore_missing_imports = true -[[tool.mypy.overrides]] -module = 'passlib.*' -ignore_missing_imports = true [tool.bandit] skips = ['B101'] diff --git a/requirements-dev.txt b/requirements-dev.txt index 4b523c35..0a58eadb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,10 @@ -r requirements-all.txt invoke +build mypy flake8 +flake8-annotations flake8-bugbear autopep8 bandit[toml] @@ -15,5 +17,5 @@ rope types-certifi types-protobuf types-redis -types-setuptools types-toml +types-passlib diff --git a/setup.py b/setup.py deleted file mode 100644 index 5a0580a7..00000000 --- a/setup.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2022 Ian C. Good -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# - -from setuptools import setup, find_packages - -with open('README.md') as f: - readme = f.read() - -with open('LICENSE.md') as f: - license = f.read() - -setup(name='pymap', - version='0.28.0', - author='Ian Good', - author_email='ian@icgood.net', - description='Lightweight, asynchronous IMAP serving in Python.', - long_description=readme + license, - long_description_content_type='text/markdown', - license='MIT', - url='https://github.com/icgood/pymap/', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Topic :: Communications :: Email :: Post-Office', - 'Topic :: Communications :: Email :: Post-Office :: IMAP', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.11'], - python_requires='~=3.11', - include_package_data=True, - packages=find_packages(include=('pymap', 'pymap.*')), - install_requires=[ - 'pysasl ~= 1.0', - 'proxy-protocol ~= 0.9.0'], - extras_require={ - 'admin': ['pymap-admin ~= 0.9.0', - 'protobuf', - 'googleapis-common-protos'], - 'macaroon': ['pymacaroons'], - 'redis': ['redis ~= 4.2', 'msgpack ~= 1.0'], - 'sieve': ['sievelib'], - 'swim': ['swim-protocol ~= 0.3.10'], - 'systemd': ['systemd-python'], - 'optional': ['hiredis', 'passlib', 'pid']}, - entry_points={ - 'console_scripts': [ - 'pymap = pymap.main:main'], - 'pymap.backend': [ - 'dict = pymap.backend.dict:DictBackend', - 'maildir = pymap.backend.maildir:MaildirBackend', - 'redis = pymap.backend.redis:RedisBackend [redis]'], - 'pymap.service': [ - 'imap = pymap.imap:IMAPService', - 'admin = pymap.admin:AdminService [admin]', - 'managesieve = pymap.sieve.manage:ManageSieveService [sieve]', - 'swim = pymap.cluster.swim:SwimService [swim]'], - 'pymap.filter': [ - 'sieve = pymap.sieve:SieveCompiler [sieve]'], - 'pymap.token': [ - 'macaroon = pymap.token.macaroon:MacaroonTokens [macaroon]'], - 'pymap.admin.handlers': [ - 'server = pymap.admin.handlers.system:SystemHandlers', - 'mailbox = pymap.admin.handlers.mailbox:MailboxHandlers', - 'user = pymap.admin.handlers.user:UserHandlers']}) diff --git a/tasks/__init__.py b/tasks/__init__.py index 66ea6182..a48c91d1 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -1,14 +1,9 @@ # type: ignore -import inspect import os import os.path from shlex import join -if not hasattr(inspect, 'getargspec'): - # https://github.com/pyinvoke/invoke/issues/833 - inspect.getargspec = inspect.getfullargspec - from invoke import task, Collection from . import check, doc, lint, test, types diff --git a/tasks/lint.py b/tasks/lint.py index e566b4e4..fac4cfe3 100644 --- a/tasks/lint.py +++ b/tasks/lint.py @@ -8,7 +8,7 @@ @task(check_import) def flake8(ctx): """Run the flake8 linter.""" - ctx.run('flake8 {} test {} *.py'.format(ctx.package, __package__)) + ctx.run('flake8 {} test {}'.format(ctx.package, __package__)) @task(check_import) diff --git a/test/server/base.py b/test/server/base.py index 88205b0c..7e5b128a 100644 --- a/test/server/base.py +++ b/test/server/base.py @@ -1,11 +1,14 @@ import asyncio from argparse import Namespace +from asyncio import StreamReader, StreamWriter +from collections.abc import Iterable import pytest from pysasl.hashing import BuiltinHash from pymap.backend.dict import DictBackend +from pymap.concurrent import Event from pymap.context import subsystem from pymap.imap import IMAPServer from pymap.sieve.manage import ManageSieveServer @@ -38,7 +41,7 @@ class TestBase: def init(cls, request, backend): test = request.instance test._fd = 1 - test.matches: dict[str, bytes] = {} + test._matches = {} @pytest.fixture def imap_server(self, backend): @@ -70,24 +73,28 @@ def _incr_fd(self): self._fd += 1 return fd - def new_transport(self, server): + @property + def matches(self) -> dict[str, bytes]: + return self._matches # type: ignore + + def new_transport(self, server: IMAPServer | ManageSieveServer) \ + -> MockTransport: return MockTransport(server, self.matches, self._incr_fd()) - def new_events(self, n=1): - if n == 1: - return subsystem.get().new_event() - else: - return (subsystem.get().new_event() for _ in range(n)) + def new_events(self, n: int) -> Iterable[Event]: + return (subsystem.get().new_event() for _ in range(n)) - def _check_queue(self, transport): + def _check_queue(self, transport: MockTransport) -> None: queue = transport.queue assert 0 == len(queue), 'Items left on queue: ' + repr(queue) - async def _run_transport(self, transport): + async def _run_transport(self, transport: MockTransport) -> None: server = transport.server - return await server(transport, transport) + reader: StreamReader = transport # type: ignore + writer: StreamWriter = transport # type: ignore + return await server(reader, writer) - async def run(self, *transports): + async def run(self, *transports: MockTransport) -> None: failures = [] transport_tasks = [asyncio.create_task( self._run_transport(transport)) for transport in transports] diff --git a/test/server/mocktransport.py b/test/server/mocktransport.py index 795d0c47..62b49184 100644 --- a/test/server/mocktransport.py +++ b/test/server/mocktransport.py @@ -7,6 +7,11 @@ import traceback from collections import deque from itertools import zip_longest +from typing import overload, Any, Literal, NoReturn + +from pymap.concurrent import Event +from pymap.imap import IMAPServer +from pymap.sieve.manage import ManageSieveServer __all__ = ['MockTransport'] @@ -24,47 +29,68 @@ def __init__(self, fd: int) -> None: self.fd = fd self.family = socket.AF_INET - def fileno(self): + def fileno(self) -> int: return self.fd +_WriteDataTuple = tuple['_WriteDataPart', ...] +_WriteDataPart = bytes | int | _WriteDataTuple | None +_WriteData = bytes | int | _WriteDataTuple +_ReadLineOp = tuple[Literal[_Type.READLINE], str, bytes, + Event | None, Event | None] +_ReadExactlyOp = tuple[Literal[_Type.READEXACTLY], str, bytes, + Event | None, Event | None] +_DrainOp = tuple[Literal[_Type.DRAIN], str, _WriteDataTuple, + Event | None, Event | None] +_ReadEofOp = tuple[Literal[_Type.READ_EOF], str, None, + Event | None, Event | None] +_Operation = _ReadLineOp | _ReadExactlyOp | _DrainOp | _ReadEofOp +_Server = IMAPServer | ManageSieveServer + + class MockTransport: - def __init__(self, server, matches, fd): + def __init__(self, server: _Server, matches: dict[str, bytes], fd) -> None: self.server = server - self.queue = deque() + self.queue: deque[_Operation] = deque() self.matches = matches self.socket = _Socket(fd) - self._write_batch = [] + self._write_batch: list[bytes] = [] self._select_count = 0 @classmethod - def _caller(cls, frame): + def _caller(cls, frame) -> str: frame = frame.f_back if frame else None fields = inspect.getframeinfo(frame) if frame else ('?', '?') return '{0}:{1!s}'.format(fields[0], fields[1]) @classmethod - def _fail(cls, msg): + def _fail(cls, msg: str) -> NoReturn: raise AssertionError(msg) - def push_readline(self, data: bytes, wait=None, set=None) -> None: + def push_readline(self, data: bytes, wait: Event | None = None, + set: Event | None = None) -> None: where = self._caller(inspect.currentframe()) self.queue.append((_Type.READLINE, where, data, wait, set)) - def push_readexactly(self, data: bytes, wait=None, set=None) -> None: + def push_readexactly(self, data: bytes, wait: Event | None = None, + set: Event | None = None) -> None: where = self._caller(inspect.currentframe()) self.queue.append((_Type.READEXACTLY, where, data, wait, set)) - def push_write(self, *data, wait=None, set=None) -> None: + def push_write(self, *data: _WriteDataPart, wait: Event | None = None, + set: Event | None = None) -> None: where = self._caller(inspect.currentframe()) self.queue.append((_Type.DRAIN, where, data, wait, set)) - def push_read_eof(self, wait=None, set=None): + def push_read_eof(self, wait: Event | None = None, + set: Event | None = None) -> None: where = self._caller(inspect.currentframe()) self.queue.append((_Type.READ_EOF, where, None, wait, set)) - def push_login(self, password=b'testpass', wait=None, set=None): + def push_login(self, password: bytes = b'testpass', + wait: Event | None = None, + set: Event | None = None) -> None: self.push_write( b'* OK [CAPABILITY IMAP4rev1', (br'(?:\s+[a-zA-Z0-9=+-]+)*', ), @@ -77,19 +103,25 @@ def push_login(self, password=b'testpass', wait=None, set=None): (br'(?:\s+[a-zA-Z0-9=+-]+)*', ), b'] Authentication successful.\r\n', set=set) - def push_logout(self, wait=None, set=None): + def push_logout(self, wait: Event | None = None, set: Event | None = None): self.push_readline( b'logout1 LOGOUT\r\n', wait=wait) self.push_write( b'* BYE Logging out.\r\n' b'logout1 OK Logout successful.\r\n', set=set) - def push_select(self, mailbox, exists=None, recent=None, uidnext=None, - unseen=None, readonly=False, examine=False, wait=None, - post_wait=None, set=None): + def push_select(self, mailbox: bytes, + exists: _WriteData | None = None, + recent: _WriteData | None = None, + uidnext: _WriteData | None = None, + unseen: _WriteData | Literal[False] | None = None, + readonly: bool = False, examine: bool = False, + wait: Event | None = None, + post_wait: Event | None = None, + set: Event | None = None) -> None: n = self._select_count = self._select_count + 1 if unseen is False: - unseen_line = (None, b'') + unseen_line: _WriteData = (None, b'') elif unseen is None: unseen_line = (None, b'* OK [UNSEEN ', (br'\d+',), b'] First unseen message.\r\n') @@ -132,7 +164,23 @@ def push_select(self, mailbox, exists=None, recent=None, uidnext=None, tag, b' OK [', ok_code, b'] Selected mailbox.\r\n', wait=post_wait, set=set) - def _pop_expected(self, got): + @overload + def _pop_expected(self, got: Literal[_Type.READLINE]) -> _ReadLineOp: + ... + + @overload + def _pop_expected(self, got: Literal[_Type.READEXACTLY]) -> _ReadExactlyOp: + ... + + @overload + def _pop_expected(self, got: Literal[_Type.DRAIN]) -> _DrainOp: + ... + + @overload + def _pop_expected(self, got: Literal[_Type.READ_EOF]) -> _ReadEofOp: + ... + + def _pop_expected(self, got: _Type) -> _Operation: try: try: type_, where, data, wait, set = self.queue.popleft() @@ -146,9 +194,10 @@ def _pop_expected(self, got): except AssertionError: traceback.print_exc() raise - return where, data, wait, set + return got, where, data, wait, set # type: ignore - def _match_write_expected(self, expected, re_parts): + def _match_write_expected(self, expected: _WriteDataTuple, + re_parts: list[bytes]) -> None: for part in expected: if part is None: re_parts.append(br'.*?') @@ -158,14 +207,18 @@ def _match_write_expected(self, expected, re_parts): re_parts.append(b'%i' % part) else: if len(part) == 1 or part[1] is None: + assert isinstance(part[0], bytes) re_parts.append(part[0]) elif part[0] is None: self._match_write_expected(part[1:], re_parts) else: regex, name = part + assert isinstance(regex, bytes) + assert isinstance(name, bytes) re_parts.append(br'(?P<' + name + br'>' + regex + br')') - def _match_write_msg(self, expected, data, full_regex, where): + def _match_write_msg(self, expected: _WriteData, data: bytes, + full_regex: bytes, where: str) -> str: parts = ['', 'Expected: ' + repr(expected), 'Got: ' + repr((data, )), @@ -185,8 +238,9 @@ def _match_write_msg(self, expected, data, full_regex, where): parts.append('') return '\n'.join(parts) - def _match_write(self, where, expected, data): - re_parts = [] + def _match_write(self, where: str, expected: _WriteDataTuple, + data: bytes) -> None: + re_parts: list[bytes] = [] self._match_write_expected(expected, re_parts) full_regex = b'^' + b''.join(re_parts) + b'$' match = re.search(full_regex, data) @@ -195,7 +249,7 @@ def _match_write(self, where, expected, data): self._match_write_msg(expected, data, full_regex, where)) self.matches.update(match.groupdict()) - def get_extra_info(self, name: str, default=None): + def get_extra_info(self, name: str, default: Any = None) -> Any: if name == 'socket': return self.socket elif name == 'peername': @@ -204,7 +258,7 @@ def get_extra_info(self, name: str, default=None): return ('5.6.7.8', 5678) async def readline(self) -> bytes: - where, data, wait, set = self._pop_expected(_Type.READLINE) + _, where, data, wait, set = self._pop_expected(_Type.READLINE) if set: set.set() if wait: @@ -217,7 +271,7 @@ async def readline(self) -> bytes: for key, val in self.matches.items()} async def readexactly(self, size: int) -> bytes: - where, data, wait, set = self._pop_expected(_Type.READEXACTLY) + _, where, data, wait, set = self._pop_expected(_Type.READEXACTLY) if size != len(data): raise AssertionError('\nExpected: ' + repr(len(data)) + '\nGot: ' + repr(size) + @@ -236,7 +290,7 @@ def write(self, data: bytes) -> None: self._write_batch.append(data) async def drain(self) -> None: - where, expected, wait, set = self._pop_expected(_Type.DRAIN) + _, where, expected, wait, set = self._pop_expected(_Type.DRAIN) data = b''.join(self._write_batch) self._write_batch = [] self._match_write(where, expected, data) @@ -248,7 +302,7 @@ async def drain(self) -> None: except asyncio.TimeoutError: self._fail('\nTimeout: 1.0s') - def at_eof(self): + def at_eof(self) -> Literal[False]: return False def close(self) -> None: diff --git a/test/server/test_admin_auth.py b/test/server/test_admin_auth.py index f48f37d8..938e2f74 100644 --- a/test/server/test_admin_auth.py +++ b/test/server/test_admin_auth.py @@ -9,6 +9,7 @@ from pymap.admin.handlers.system import SystemHandlers from pymap.admin.handlers.user import UserHandlers +from pymap.interfaces.backend import BackendInterface from .base import TestBase @@ -23,7 +24,7 @@ class TestAdminAuth(TestBase): def overrides(self): return {'admin_key': b'testadmintoken'} - async def test_token(self, backend) -> None: + async def test_token(self, backend: BackendInterface) -> None: token = await self._login(backend, 'testuser', 'testpass') await self._get_user(backend, token, 'testuser') await self._set_user(backend, token, 'testuser', 'newpass') @@ -34,7 +35,7 @@ async def test_token(self, backend) -> None: await self._get_user(backend, self.admin_token, 'testuser', failure_key='UserNotFound') - async def test_authorization(self, backend) -> None: + async def test_authorization(self, backend: BackendInterface) -> None: await self._set_user(backend, self.admin_token, 'newuser', 'newpass') token1 = await self._login(backend, 'testuser', 'testpass') token2 = await self._login(backend, 'newuser', 'newpass') @@ -48,7 +49,7 @@ async def test_authorization(self, backend) -> None: failure_key='AuthorizationFailure') await self._delete_user(backend, token2, 'newuser') - async def test_admin_role(self, backend) -> None: + async def test_admin_role(self, backend: BackendInterface) -> None: token = await self._login(backend, 'testuser', 'testpass') await self._set_user(backend, token, 'testuser', 'testpass', params={'role': 'admin'}, @@ -69,8 +70,9 @@ async def _login(self, backend, authcid: str, secret: str) -> str: assert SUCCESS == response.result.code return response.bearer_token - async def _get_user(self, backend, token: str, user: str, *, - failure_key: str = None) -> None: + async def _get_user(self, backend: BackendInterface, + token: str, user: str, *, + failure_key: str | None = None) -> None: handlers = UserHandlers(backend) request = GetUserRequest(user=user) metadata = {'auth-token': token} @@ -84,9 +86,10 @@ async def _get_user(self, backend, token: str, user: str, *, assert FAILURE == response.result.code assert failure_key == response.result.key - async def _set_user(self, backend, token: str, user: str, password: str, *, - params: Mapping[str, str] = None, - failure_key: str = None) -> None: + async def _set_user(self, backend: BackendInterface, + token: str, user: str, password: str, *, + params: Mapping[str, str] | None = None, + failure_key: str | None = None) -> None: handlers = UserHandlers(backend) data = UserData(password=password, params=params) request = SetUserRequest(user=user, data=data) @@ -101,8 +104,9 @@ async def _set_user(self, backend, token: str, user: str, password: str, *, assert FAILURE == response.result.code assert failure_key == response.result.key - async def _delete_user(self, backend, token: str, user: str, *, - failure_key: str = None) -> None: + async def _delete_user(self, backend: BackendInterface, + token: str, user: str, *, + failure_key: str | None = None) -> None: handlers = UserHandlers(backend) request = DeleteUserRequest(user=user) metadata = {'auth-token': token} diff --git a/test/server/test_admin_mailbox.py b/test/server/test_admin_mailbox.py index 8503d49a..544646f8 100644 --- a/test/server/test_admin_mailbox.py +++ b/test/server/test_admin_mailbox.py @@ -5,6 +5,8 @@ from pymapadmin.grpc.admin_pb2 import AppendRequest, SUCCESS, FAILURE from pymap.admin.handlers.mailbox import MailboxHandlers +from pymap.imap import IMAPServer +from pymap.interfaces.backend import BackendInterface from .base import TestBase @@ -20,7 +22,8 @@ class TestMailboxHandlers(TestBase): def overrides(self): return {'admin_key': b'testadmintoken'} - async def test_append(self, backend, imap_server) -> None: + async def test_append(self, backend: BackendInterface, + imap_server: IMAPServer) -> None: handlers = MailboxHandlers(backend) data = b'From: user@example.com\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -50,7 +53,8 @@ async def test_append(self, backend, imap_server) -> None: transport.push_logout() await self.run(transport) - async def test_append_user_not_found(self, backend) -> None: + async def test_append_user_not_found(self, backend: BackendInterface) \ + -> None: handlers = MailboxHandlers(backend) request = AppendRequest(user='baduser') async with ChannelFor([handlers]) as channel: @@ -59,7 +63,8 @@ async def test_append_user_not_found(self, backend) -> None: assert FAILURE == response.result.code assert 'UserNotFound' == response.result.key - async def test_append_mailbox_not_found(self, backend) -> None: + async def test_append_mailbox_not_found(self, backend: BackendInterface) \ + -> None: handlers = MailboxHandlers(backend) request = AppendRequest(user='testuser', mailbox='BAD') async with ChannelFor([handlers]) as channel: @@ -69,7 +74,8 @@ async def test_append_mailbox_not_found(self, backend) -> None: assert 'BAD' == response.mailbox assert 'MailboxNotFound' == response.result.key - async def test_append_filter_reject(self, backend) -> None: + async def test_append_filter_reject(self, backend: BackendInterface) \ + -> None: handlers = MailboxHandlers(backend) data = b'Subject: reject this\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -81,7 +87,8 @@ async def test_append_filter_reject(self, backend) -> None: assert FAILURE == response.result.code assert 'AppendFailure' == response.result.key - async def test_append_filter_discard(self, backend) -> None: + async def test_append_filter_discard(self, backend: BackendInterface) \ + -> None: handlers = MailboxHandlers(backend) data = b'Subject: discard this\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -94,7 +101,8 @@ async def test_append_filter_discard(self, backend) -> None: assert not response.mailbox assert not response.uid - async def test_append_filter_address_is(self, backend) -> None: + async def test_append_filter_address_is(self, backend: BackendInterface) \ + -> None: handlers = MailboxHandlers(backend) data = b'From: foo@example.com\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -105,7 +113,8 @@ async def test_append_filter_address_is(self, backend) -> None: response = await stub.Append(request, metadata=self.metadata) assert 'Test 1' == response.mailbox - async def test_append_filter_address_contains(self, backend) -> None: + async def test_append_filter_address_contains( + self, backend: BackendInterface) -> None: handlers = MailboxHandlers(backend) data = b'From: user@foo.com\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -116,7 +125,8 @@ async def test_append_filter_address_contains(self, backend) -> None: response = await stub.Append(request, metadata=self.metadata) assert 'Test 2' == response.mailbox - async def test_append_filter_address_matches(self, backend) -> None: + async def test_append_filter_address_matches( + self, backend: BackendInterface) -> None: handlers = MailboxHandlers(backend) data = b'To: bigfoot@example.com\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -127,7 +137,8 @@ async def test_append_filter_address_matches(self, backend) -> None: response = await stub.Append(request, metadata=self.metadata) assert 'Test 3' == response.mailbox - async def test_append_filter_envelope_is(self, backend) -> None: + async def test_append_filter_envelope_is( + self, backend: BackendInterface) -> None: handlers = MailboxHandlers(backend) data = b'From: user@example.com\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -139,7 +150,8 @@ async def test_append_filter_envelope_is(self, backend) -> None: response = await stub.Append(request, metadata=self.metadata) assert 'Test 4' == response.mailbox - async def test_append_filter_envelope_contains(self, backend) -> None: + async def test_append_filter_envelope_contains( + self, backend: BackendInterface) -> None: handlers = MailboxHandlers(backend) data = b'From: user@example.com\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -151,7 +163,8 @@ async def test_append_filter_envelope_contains(self, backend) -> None: response = await stub.Append(request, metadata=self.metadata) assert 'Test 5' == response.mailbox - async def test_append_filter_envelope_matches(self, backend) -> None: + async def test_append_filter_envelope_matches( + self, backend: BackendInterface) -> None: handlers = MailboxHandlers(backend) data = b'From: user@example.com\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -163,7 +176,8 @@ async def test_append_filter_envelope_matches(self, backend) -> None: response = await stub.Append(request, metadata=self.metadata) assert 'Test 6' == response.mailbox - async def test_append_filter_exists(self, backend) -> None: + async def test_append_filter_exists(self, backend: BackendInterface) \ + -> None: handlers = MailboxHandlers(backend) data = b'X-Foo: foo\nX-Bar: bar\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -174,7 +188,8 @@ async def test_append_filter_exists(self, backend) -> None: response = await stub.Append(request, metadata=self.metadata) assert 'Test 7' == response.mailbox - async def test_append_filter_header(self, backend) -> None: + async def test_append_filter_header(self, backend: BackendInterface) \ + -> None: handlers = MailboxHandlers(backend) data = b'X-Caffeine: C8H10N4O2\n\ntest message!\n' request = AppendRequest(user='testuser', mailbox='INBOX', @@ -185,7 +200,7 @@ async def test_append_filter_header(self, backend) -> None: response = await stub.Append(request, metadata=self.metadata) assert 'Test 8' == response.mailbox - async def test_append_filter_size(self, backend) -> None: + async def test_append_filter_size(self, backend: BackendInterface) -> None: handlers = MailboxHandlers(backend) data = b'From: user@example.com\n\ntest message!\n' data = data + b'x' * (1234 - len(data)) diff --git a/test/server/test_admin_system.py b/test/server/test_admin_system.py index a0174ddf..338573fa 100644 --- a/test/server/test_admin_system.py +++ b/test/server/test_admin_system.py @@ -1,19 +1,20 @@ from grpclib.testing import ChannelFor +from pymapadmin import __version__ as pymap_admin_version from pymapadmin.grpc.admin_grpc import SystemStub from pymapadmin.grpc.admin_pb2 import SUCCESS, FAILURE, \ LoginRequest, PingRequest from pymap import __version__ as pymap_version -from pymapadmin import __version__ as pymap_admin_version from pymap.admin.handlers.system import SystemHandlers +from pymap.interfaces.backend import BackendInterface from .base import TestBase class TestSystemHandlers(TestBase): - async def test_login(self, backend) -> None: + async def test_login(self, backend: BackendInterface) -> None: handlers = SystemHandlers(backend) request = LoginRequest(authcid='testuser', secret='testpass') async with ChannelFor([handlers]) as channel: @@ -22,7 +23,7 @@ async def test_login(self, backend) -> None: assert SUCCESS == response.result.code assert response.bearer_token - async def test_login_failure(self, backend) -> None: + async def test_login_failure(self, backend: BackendInterface) -> None: handlers = SystemHandlers(backend) request = LoginRequest(authcid='baduser', secret='badpass') async with ChannelFor([handlers]) as channel: @@ -32,7 +33,7 @@ async def test_login_failure(self, backend) -> None: assert 'InvalidAuth' == response.result.key assert not response.HasField('bearer_token') - async def test_ping(self, backend) -> None: + async def test_ping(self, backend: BackendInterface) -> None: handlers = SystemHandlers(backend) request = PingRequest() async with ChannelFor([handlers]) as channel: diff --git a/test/server/test_admin_user.py b/test/server/test_admin_user.py index 7c127534..dd9952f2 100644 --- a/test/server/test_admin_user.py +++ b/test/server/test_admin_user.py @@ -6,6 +6,8 @@ GetUserRequest, SetUserRequest, DeleteUserRequest, UserData from pymap.admin.handlers.user import UserHandlers +from pymap.imap import IMAPServer +from pymap.interfaces.backend import BackendInterface from .base import TestBase @@ -21,7 +23,7 @@ class TestMailboxHandlers(TestBase): def overrides(self): return {'admin_key': b'testadmintoken'} - async def test_get_user(self, backend) -> None: + async def test_get_user(self, backend: BackendInterface) -> None: handlers = UserHandlers(backend) request = GetUserRequest(user='testuser') async with ChannelFor([handlers]) as channel: @@ -32,7 +34,7 @@ async def test_get_user(self, backend) -> None: assert '$pbkdf2$1$$FzEpdTtdOaIFkUucxV4PjfW88BE=' \ == response.data.password - async def test_get_user_not_found(self, backend) -> None: + async def test_get_user_not_found(self, backend: BackendInterface) -> None: handlers = UserHandlers(backend) request = GetUserRequest(user='baduser') async with ChannelFor([handlers]) as channel: @@ -41,7 +43,8 @@ async def test_get_user_not_found(self, backend) -> None: assert FAILURE == response.result.code assert 'UserNotFound' == response.result.key - async def test_set_user(self, backend, imap_server) -> None: + async def test_set_user(self, backend: BackendInterface, + imap_server: IMAPServer) -> None: handlers = UserHandlers(backend) data = UserData(password='newpass', params={'key': 'val'}) request = SetUserRequest(user='testuser', data=data) @@ -57,7 +60,7 @@ async def test_set_user(self, backend, imap_server) -> None: transport.push_logout() await self.run(transport) - async def test_delete_user(self, backend) -> None: + async def test_delete_user(self, backend: BackendInterface) -> None: handlers = UserHandlers(backend) request = DeleteUserRequest(user='testuser') async with ChannelFor([handlers]) as channel: @@ -66,7 +69,8 @@ async def test_delete_user(self, backend) -> None: assert SUCCESS == response.result.code assert 'testuser' == response.username - async def test_delete_user_not_found(self, backend) -> None: + async def test_delete_user_not_found(self, backend: BackendInterface) \ + -> None: handlers = UserHandlers(backend) request = DeleteUserRequest(user='baduser') async with ChannelFor([handlers]) as channel: diff --git a/test/server/test_copy.py b/test/server/test_copy.py index b5f2b09d..86ebc216 100644 --- a/test/server/test_copy.py +++ b/test/server/test_copy.py @@ -1,10 +1,12 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestCopy(TestBase): - async def test_copy(self, imap_server): + async def test_copy(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -17,7 +19,7 @@ async def test_copy(self, imap_server): transport.push_logout() await self.run(transport) - async def test_uid_copy(self, imap_server): + async def test_uid_copy(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -30,7 +32,7 @@ async def test_uid_copy(self, imap_server): transport.push_logout() await self.run(transport) - async def test_copy_email_id(self, imap_server): + async def test_copy_email_id(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -55,7 +57,8 @@ async def test_copy_email_id(self, imap_server): assert self.matches['mid1'] == self.matches['mid2'] assert self.matches['tid1'] == self.matches['tid2'] - async def test_concurrent_copy_fetch(self, imap_server): + async def test_concurrent_copy_fetch(self, imap_server: IMAPServer) \ + -> None: transport = self.new_transport(imap_server) concurrent = self.new_transport(imap_server) event1, event2, event3 = self.new_events(3) @@ -85,7 +88,7 @@ async def test_concurrent_copy_fetch(self, imap_server): await self.run(transport, concurrent) - async def test_move(self, imap_server): + async def test_move(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -103,7 +106,7 @@ async def test_move(self, imap_server): transport.push_logout() await self.run(transport) - async def test_uid_move(self, imap_server): + async def test_uid_move(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -121,7 +124,7 @@ async def test_uid_move(self, imap_server): transport.push_logout() await self.run(transport) - async def test_move_email_id(self, imap_server): + async def test_move_email_id(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -150,7 +153,8 @@ async def test_move_email_id(self, imap_server): assert self.matches['mid1'] == self.matches['mid2'] assert self.matches['tid1'] == self.matches['tid2'] - async def test_concurrent_move_fetch(self, imap_server): + async def test_concurrent_move_fetch(self, imap_server: IMAPServer) \ + -> None: transport = self.new_transport(imap_server) concurrent = self.new_transport(imap_server) event1, event2, event3 = self.new_events(3) diff --git a/test/server/test_expunge.py b/test/server/test_expunge.py index 17b11912..f39e0c4e 100644 --- a/test/server/test_expunge.py +++ b/test/server/test_expunge.py @@ -1,10 +1,12 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestExpunge(TestBase): - async def test_expunge(self, imap_server): + async def test_expunge(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -22,7 +24,7 @@ async def test_expunge(self, imap_server): transport.push_logout() await self.run(transport) - async def test_expunge_uid(self, imap_server): + async def test_expunge_uid(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -44,7 +46,8 @@ async def test_expunge_uid(self, imap_server): transport.push_logout() await self.run(transport) - async def test_concurrent_expunge_responses(self, imap_server): + async def test_concurrent_expunge_responses( + self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) concurrent = self.new_transport(imap_server) event1, event2 = self.new_events(2) diff --git a/test/server/test_fetch.py b/test/server/test_fetch.py index 720e7d28..b78d3d51 100644 --- a/test/server/test_fetch.py +++ b/test/server/test_fetch.py @@ -1,10 +1,12 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestFetch(TestBase): - async def test_uid_fetch(self, imap_server): + async def test_uid_fetch(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -19,7 +21,7 @@ async def test_uid_fetch(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_full(self, imap_server): + async def test_fetch_full(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -39,7 +41,7 @@ async def test_fetch_full(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_bodystructure(self, imap_server): + async def test_fetch_bodystructure(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -52,7 +54,7 @@ async def test_fetch_bodystructure(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_body_section(self, imap_server): + async def test_fetch_body_section(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -77,7 +79,7 @@ async def test_fetch_body_section(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_rfc822(self, imap_server): + async def test_fetch_rfc822(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -102,7 +104,7 @@ async def test_fetch_rfc822(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_rfc822_header(self, imap_server): + async def test_fetch_rfc822_header(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -120,7 +122,7 @@ async def test_fetch_rfc822_header(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_rfc822_text(self, imap_server): + async def test_fetch_rfc822_text(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -139,7 +141,8 @@ async def test_fetch_rfc822_text(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_body_section_header(self, imap_server): + async def test_fetch_body_section_header(self, imap_server: IMAPServer) \ + -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Sent') @@ -164,7 +167,8 @@ async def test_fetch_body_section_header(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_body_section_header_fields(self, imap_server): + async def test_fetch_body_section_header_fields( + self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Sent') @@ -180,7 +184,8 @@ async def test_fetch_body_section_header_fields(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_body_section_text(self, imap_server): + async def test_fetch_body_section_text( + self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Sent') @@ -205,7 +210,7 @@ async def test_fetch_body_section_text(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_binary_section(self, imap_server): + async def test_fetch_binary_section(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Sent') @@ -226,7 +231,8 @@ async def test_fetch_binary_section(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_binary_section_partial(self, imap_server): + async def test_fetch_binary_section_partial( + self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Sent') @@ -240,7 +246,7 @@ async def test_fetch_binary_section_partial(self, imap_server): transport.push_logout() await self.run(transport) - async def test_append_fetch_binary(self, imap_server): + async def test_append_fetch_binary(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) message = b'\r\ntest\x00message\r\n' transport.push_login() @@ -265,7 +271,7 @@ async def test_append_fetch_binary(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_email_id(self, imap_server): + async def test_fetch_email_id(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') diff --git a/test/server/test_idle.py b/test/server/test_idle.py index 5b82e9e7..9a193515 100644 --- a/test/server/test_idle.py +++ b/test/server/test_idle.py @@ -1,10 +1,12 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestIdle(TestBase): - async def test_idle(self, imap_server): + async def test_idle(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX', 4, 1, 105) @@ -20,7 +22,7 @@ async def test_idle(self, imap_server): transport.push_logout() await self.run(transport) - async def test_idle_invalid(self, imap_server): + async def test_idle_invalid(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX', 4, 1, 105) @@ -36,7 +38,7 @@ async def test_idle_invalid(self, imap_server): transport.push_logout() await self.run(transport) - async def test_idle_noselect(self, imap_server): + async def test_idle_noselect(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -46,7 +48,8 @@ async def test_idle_noselect(self, imap_server): transport.push_logout() await self.run(transport) - async def test_concurrent_idle_append(self, imap_server): + async def test_concurrent_idle_append(self, imap_server: IMAPServer) \ + -> None: transport = self.new_transport(imap_server) concurrent = self.new_transport(imap_server) event1, event2, event3 = self.new_events(3) @@ -86,7 +89,8 @@ async def test_concurrent_idle_append(self, imap_server): await self.run(transport, concurrent) - async def test_concurrent_idle_expunge(self, imap_server): + async def test_concurrent_idle_expunge(self, imap_server: IMAPServer) \ + -> None: transport = self.new_transport(imap_server) concurrent = self.new_transport(imap_server) event1, event2, event3 = self.new_events(3) diff --git a/test/server/test_mailbox.py b/test/server/test_mailbox.py index b17f7be6..e583fd8c 100644 --- a/test/server/test_mailbox.py +++ b/test/server/test_mailbox.py @@ -3,10 +3,12 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestMailbox(TestBase): - async def test_list_sep(self, imap_server): + async def test_list_sep(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -17,7 +19,7 @@ async def test_list_sep(self, imap_server): transport.push_logout() await self.run(transport) - async def test_list(self, imap_server): + async def test_list(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -30,7 +32,7 @@ async def test_list(self, imap_server): transport.push_logout() await self.run(transport) - async def test_create(self, imap_server): + async def test_create(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -49,7 +51,7 @@ async def test_create(self, imap_server): transport.push_logout() await self.run(transport) - async def test_create_inferior(self, imap_server): + async def test_create_inferior(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -66,7 +68,7 @@ async def test_create_inferior(self, imap_server): transport.push_logout() await self.run(transport) - async def test_delete(self, imap_server): + async def test_delete(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -82,7 +84,7 @@ async def test_delete(self, imap_server): transport.push_logout() await self.run(transport) - async def test_delete_superior(self, imap_server): + async def test_delete_superior(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -103,7 +105,7 @@ async def test_delete_superior(self, imap_server): transport.push_logout() await self.run(transport) - async def test_delete_selected(self, imap_server): + async def test_delete_selected(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Sent') @@ -114,7 +116,7 @@ async def test_delete_selected(self, imap_server): b'delete1 OK DELETE completed.\r\n') await self.run(transport) - async def test_lsub(self, imap_server): + async def test_lsub(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -125,7 +127,8 @@ async def test_lsub(self, imap_server): transport.push_logout() await self.run(transport) - async def test_subscribe_unsubscribe(self, imap_server): + async def test_subscribe_unsubscribe(self, imap_server: IMAPServer) \ + -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -149,7 +152,7 @@ async def test_subscribe_unsubscribe(self, imap_server): transport.push_logout() await self.run(transport) - async def test_status(self, imap_server): + async def test_status(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -184,7 +187,7 @@ async def test_status(self, imap_server): assert self.matches['uidval1'] == self.matches['uidval2'] assert self.matches['mbxid1'] == self.matches['mbxid'] - async def test_append(self, imap_server): + async def test_append(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) message = b'test message\r\n' transport.push_login() @@ -202,7 +205,7 @@ async def test_append(self, imap_server): transport.push_logout() await self.run(transport) - async def test_append_empty(self, imap_server): + async def test_append_empty(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -219,7 +222,7 @@ async def test_append_empty(self, imap_server): transport.push_logout() await self.run(transport) - async def test_append_multi(self, imap_server): + async def test_append_multi(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) message_1 = b'test message\r\n' message_2 = b'other test message\r\n' @@ -243,7 +246,7 @@ async def test_append_multi(self, imap_server): transport.push_logout() await self.run(transport) - async def test_append_selected(self, imap_server): + async def test_append_selected(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) message = b'test message\r\n' transport.push_login() @@ -269,7 +272,7 @@ async def test_append_selected(self, imap_server): transport.push_logout() await self.run(transport) - async def test_append_email_id(self, imap_server): + async def test_append_email_id(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) message_1 = b'test message\r\n' message_2 = b'other test message\r\n' @@ -315,7 +318,7 @@ async def test_append_email_id(self, imap_server): assert self.matches['id1'] != self.matches['id2'] assert self.matches['id1'] == self.matches['id3'] - async def test_append_thread_id(self, imap_server): + async def test_append_thread_id(self, imap_server: IMAPServer) -> None: messages = [dedent("""\ Message-Id: Subject: thread one diff --git a/test/server/test_managesieve.py b/test/server/test_managesieve.py index 62cc7d36..2cedba99 100644 --- a/test/server/test_managesieve.py +++ b/test/server/test_managesieve.py @@ -1,10 +1,13 @@ from .base import TestBase +from .mocktransport import MockTransport + +from pymap.sieve.manage import ManageSieveServer class TestManageSieve(TestBase): - def _push_capabilities(self, transport): + def _push_capabilities(self, transport: MockTransport) -> None: transport.push_write( b'"IMPLEMENTATION" "pymap managesieve', (br'.*?', ), b'"\r\n' b'"SASL" "PLAIN LOGIN"\r\n' @@ -13,13 +16,13 @@ def _push_capabilities(self, transport): b'"VERSION" "1.0"\r\n' b'OK\r\n') - def _push_logout(self, transport): + def _push_logout(self, transport: MockTransport) -> None: transport.push_readline( b'logout\r\n') transport.push_write( b'BYE\r\n') - def _push_authenticate(self, transport): + def _push_authenticate(self, transport: MockTransport) -> None: transport.push_readline( b'authenticate "plain"\r\n') transport.push_write( @@ -30,20 +33,23 @@ def _push_authenticate(self, transport): transport.push_write( b'OK\r\n') - async def test_capabilities(self, managesieve_server): + async def test_capabilities( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_logout(transport) await self.run(transport) - async def test_authenticate(self, managesieve_server): + async def test_authenticate( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) self._push_logout(transport) await self.run(transport) - async def test_capability(self, managesieve_server): + async def test_capability( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) transport.push_readline( @@ -62,7 +68,8 @@ async def test_capability(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_unauthenticate(self, managesieve_server): + async def test_unauthenticate( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) @@ -76,7 +83,8 @@ async def test_unauthenticate(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_bad_command(self, managesieve_server): + async def test_bad_command( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) transport.push_readline( @@ -91,7 +99,8 @@ async def test_bad_command(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_listscripts(self, managesieve_server): + async def test_listscripts( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) @@ -103,7 +112,8 @@ async def test_listscripts(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_getscript(self, managesieve_server): + async def test_getscript( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) @@ -141,7 +151,8 @@ async def test_getscript(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_havespace(self, managesieve_server): + async def test_havespace( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) @@ -152,7 +163,8 @@ async def test_havespace(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_putscript(self, managesieve_server): + async def test_putscript( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) @@ -174,7 +186,8 @@ async def test_putscript(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_deletescript(self, managesieve_server): + async def test_deletescript( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) @@ -197,7 +210,8 @@ async def test_deletescript(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_renamescript(self, managesieve_server): + async def test_renamescript( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) @@ -213,7 +227,8 @@ async def test_renamescript(self, managesieve_server): self._push_logout(transport) await self.run(transport) - async def test_checkscript(self, managesieve_server): + async def test_checkscript( + self, managesieve_server: ManageSieveServer) -> None: transport = self.new_transport(managesieve_server) self._push_capabilities(transport) self._push_authenticate(transport) diff --git a/test/server/test_readonly.py b/test/server/test_readonly.py index 55eb9ef3..664f129d 100644 --- a/test/server/test_readonly.py +++ b/test/server/test_readonly.py @@ -1,10 +1,12 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestReadOnly(TestBase): - async def test_select(self, imap_server): + async def test_select(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Trash', 1, 1, readonly=True) @@ -12,7 +14,7 @@ async def test_select(self, imap_server): transport.push_logout() await self.run(transport) - async def test_examine(self, imap_server): + async def test_examine(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX', 4, 1, examine=True) @@ -20,7 +22,7 @@ async def test_examine(self, imap_server): transport.push_logout() await self.run(transport) - async def test_append(self, imap_server): + async def test_append(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) message = b'test message\r\n' transport.push_login() @@ -37,7 +39,7 @@ async def test_append(self, imap_server): transport.push_logout() await self.run(transport) - async def test_copy(self, imap_server): + async def test_copy(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -49,7 +51,7 @@ async def test_copy(self, imap_server): transport.push_logout() await self.run(transport) - async def test_expunge(self, imap_server): + async def test_expunge(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Trash', 1, readonly=True) @@ -61,7 +63,7 @@ async def test_expunge(self, imap_server): transport.push_logout() await self.run(transport) - async def test_store(self, imap_server): + async def test_store(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Trash', 1, readonly=True) @@ -72,7 +74,7 @@ async def test_store(self, imap_server): transport.push_logout() await self.run(transport) - async def test_fetch_not_seen(self, imap_server): + async def test_fetch_not_seen(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Trash', 1, readonly=True) diff --git a/test/server/test_rename.py b/test/server/test_rename.py index b2964fd4..18d9273b 100644 --- a/test/server/test_rename.py +++ b/test/server/test_rename.py @@ -1,10 +1,12 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestMailbox(TestBase): - async def test_rename(self, imap_server): + async def test_rename(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -31,7 +33,7 @@ async def test_rename(self, imap_server): await self.run(transport) assert self.matches['uidval1'] == self.matches['uidval2'] - async def test_rename_inbox(self, imap_server): + async def test_rename_inbox(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -61,7 +63,8 @@ async def test_rename_inbox(self, imap_server): assert self.matches['uidval1'] != self.matches['uidval2'] assert self.matches['uidval1'] == self.matches['uidval3'] - async def test_rename_inbox_selected(self, imap_server): + async def test_rename_inbox_selected(self, imap_server: IMAPServer) \ + -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -73,7 +76,8 @@ async def test_rename_inbox_selected(self, imap_server): transport.push_logout() await self.run(transport) - async def test_rename_other_selected(self, imap_server): + async def test_rename_other_selected(self, imap_server: IMAPServer) \ + -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Sent') @@ -84,7 +88,7 @@ async def test_rename_other_selected(self, imap_server): b'rename1 OK RENAME completed.\r\n') await self.run(transport) - async def test_rename_selected(self, imap_server): + async def test_rename_selected(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'Sent') @@ -95,7 +99,7 @@ async def test_rename_selected(self, imap_server): b'rename1 OK RENAME completed.\r\n') await self.run(transport) - async def test_rename_inferior(self, imap_server): + async def test_rename_inferior(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( @@ -135,7 +139,7 @@ async def test_rename_inferior(self, imap_server): transport.push_logout() await self.run(transport) - async def test_rename_mailbox_id(self, imap_server): + async def test_rename_mailbox_id(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_readline( diff --git a/test/server/test_search.py b/test/server/test_search.py index 2095bc80..fb5182de 100644 --- a/test/server/test_search.py +++ b/test/server/test_search.py @@ -3,6 +3,8 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestSearch(TestBase): @@ -10,7 +12,7 @@ class TestSearch(TestBase): def overrides(self): return {'disable_search_keys': [b'DRAFT']} - async def test_search_disabled(self, imap_server): + async def test_search_disabled(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -21,7 +23,7 @@ async def test_search_disabled(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search(self, imap_server): + async def test_search(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -33,7 +35,7 @@ async def test_search(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_not(self, imap_server): + async def test_search_not(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -45,7 +47,7 @@ async def test_search_not(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_uid(self, imap_server): + async def test_search_uid(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -57,7 +59,7 @@ async def test_search_uid(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_seqset(self, imap_server): + async def test_search_seqset(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -69,7 +71,7 @@ async def test_search_seqset(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_and(self, imap_server): + async def test_search_and(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -81,7 +83,7 @@ async def test_search_and(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_or(self, imap_server): + async def test_search_or(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -93,7 +95,7 @@ async def test_search_or(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_seen(self, imap_server): + async def test_search_seen(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -105,7 +107,7 @@ async def test_search_seen(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_unseen(self, imap_server): + async def test_search_unseen(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -117,7 +119,7 @@ async def test_search_unseen(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_new(self, imap_server): + async def test_search_new(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -129,7 +131,7 @@ async def test_search_new(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_date_on(self, imap_server): + async def test_search_date_on(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -141,7 +143,7 @@ async def test_search_date_on(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_date_since(self, imap_server): + async def test_search_date_since(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -153,7 +155,7 @@ async def test_search_date_since(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_header_date(self, imap_server): + async def test_search_header_date(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -165,7 +167,7 @@ async def test_search_header_date(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_size(self, imap_server): + async def test_search_size(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -177,7 +179,7 @@ async def test_search_size(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_from(self, imap_server): + async def test_search_from(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -189,7 +191,7 @@ async def test_search_from(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_subject(self, imap_server): + async def test_search_subject(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -201,7 +203,7 @@ async def test_search_subject(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_header(self, imap_server): + async def test_search_header(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -213,7 +215,7 @@ async def test_search_header(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_body(self, imap_server): + async def test_search_body(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -225,7 +227,7 @@ async def test_search_body(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_text(self, imap_server): + async def test_search_text(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -237,7 +239,7 @@ async def test_search_text(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_emailid(self, imap_server): + async def test_search_emailid(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -260,7 +262,7 @@ async def test_search_emailid(self, imap_server): transport.push_logout() await self.run(transport) - async def test_search_threadid(self, imap_server): + async def test_search_threadid(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') diff --git a/test/server/test_session.py b/test/server/test_session.py index 3fd69f2d..fd8651d1 100644 --- a/test/server/test_session.py +++ b/test/server/test_session.py @@ -3,23 +3,25 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestSession(TestBase): - async def test_login_logout(self, imap_server): + async def test_login_logout(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_logout() await self.run(transport) - async def test_select(self, imap_server): + async def test_select(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX', 4, 1, 105, 3) transport.push_logout() await self.run(transport) - async def test_select_clears_recent(self, imap_server): + async def test_select_clears_recent(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX', 4, 1, 105, 3) @@ -27,7 +29,8 @@ async def test_select_clears_recent(self, imap_server): transport.push_logout() await self.run(transport) - async def test_concurrent_select_clears_recent(self, imap_server): + async def test_concurrent_select_clears_recent( + self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) concurrent = self.new_transport(imap_server) event1, event2 = self.new_events(2) @@ -47,7 +50,7 @@ async def test_concurrent_select_clears_recent(self, imap_server): await self.run(transport, concurrent) - async def test_auth_plain(self, imap_server): + async def test_auth_plain(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_write( b'* OK [CAPABILITY IMAP4rev1', diff --git a/test/server/test_store.py b/test/server/test_store.py index d59975a8..834beea9 100644 --- a/test/server/test_store.py +++ b/test/server/test_store.py @@ -1,10 +1,12 @@ from .base import TestBase +from pymap.imap import IMAPServer + class TestStore(TestBase): - async def test_store(self, imap_server): + async def test_store(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -16,7 +18,7 @@ async def test_store(self, imap_server): transport.push_logout() await self.run(transport) - async def test_store_silent(self, imap_server): + async def test_store_silent(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -27,7 +29,7 @@ async def test_store_silent(self, imap_server): transport.push_logout() await self.run(transport) - async def test_uid_store(self, imap_server): + async def test_uid_store(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -39,7 +41,7 @@ async def test_uid_store(self, imap_server): transport.push_logout() await self.run(transport) - async def test_store_add_recent(self, imap_server): + async def test_store_add_recent(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -51,7 +53,7 @@ async def test_store_add_recent(self, imap_server): transport.push_logout() await self.run(transport) - async def test_store_remove_recent(self, imap_server): + async def test_store_remove_recent(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -63,7 +65,7 @@ async def test_store_remove_recent(self, imap_server): transport.push_logout() await self.run(transport) - async def test_store_set_non_recent(self, imap_server): + async def test_store_set_non_recent(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX') @@ -75,7 +77,7 @@ async def test_store_set_non_recent(self, imap_server): transport.push_logout() await self.run(transport) - async def test_store_invalid(self, imap_server): + async def test_store_invalid(self, imap_server: IMAPServer) -> None: transport = self.new_transport(imap_server) transport.push_login() transport.push_select(b'INBOX', 4, 1) diff --git a/test/test_parsing_command_select.py b/test/test_parsing_command_select.py index f811c8ca..77875fe0 100644 --- a/test/test_parsing_command_select.py +++ b/test/test_parsing_command_select.py @@ -77,8 +77,8 @@ def test_parse_uid(self): def test_parse_list(self): ret, buf = FetchCommand.parse(b' 1,2,3 (FLAGS ENVELOPE)\n ', Params()) self.assertEqual([1, 2, 3], ret.sequence_set.value) - self.assertListEqual([FetchAttribute(b'FLAGS'), - FetchAttribute(b'ENVELOPE')], ret.attributes) + self.assertEqual((FetchAttribute(b'FLAGS'), + FetchAttribute(b'ENVELOPE')), ret.attributes) self.assertEqual(b' ', buf) def test_parse_uid_list(self): diff --git a/test/test_parsing_primitives.py b/test/test_parsing_primitives.py index 3500c840..dc95742b 100644 --- a/test/test_parsing_primitives.py +++ b/test/test_parsing_primitives.py @@ -184,7 +184,7 @@ def test_parse(self): def test_parse_empty(self): ret, buf = List.parse(br' () ', Params()) self.assertIsInstance(ret, List) - self.assertEqual([], ret.value) + self.assertEqual((), ret.value) self.assertEqual(b' ', buf) def test_parse_failure(self): diff --git a/test/test_parsing_specials.py b/test/test_parsing_specials.py index 560c9ff0..b3dbfc27 100644 --- a/test/test_parsing_specials.py +++ b/test/test_parsing_specials.py @@ -308,7 +308,8 @@ def test_parse_seqset(self): def test_parse_list(self): ret, buf = SearchKey.parse(b'(4,5,6 NOT 1,2,3)', Params()) self.assertEqual(b'KEYSET', ret.value) - self.assertIsInstance(ret.filter, list) + self.assertIsInstance(ret.filter, tuple) + self.assertEqual(hash(ret), hash(ret)) self.assertEqual(2, len(ret.filter)) self.assertEqual(b'SEQSET', ret.filter[0].value) self.assertIsInstance(ret.filter[0].filter, SequenceSet)