Skip to content

Commit

Permalink
Switch to hatch and importlib
Browse files Browse the repository at this point in the history
  • Loading branch information
icgood committed Mar 19, 2023
1 parent 5980157 commit 6f197aa
Show file tree
Hide file tree
Showing 43 changed files with 465 additions and 337 deletions.
4 changes: 4 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
[flake8]
extend-select = B901, B902, B903, B904
extend-ignore = ANN101, ANN102
per-file-ignores =
test/*: ANN
tasks/*: ANN, B028
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
6 changes: 3 additions & 3 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 0 additions & 5 deletions MANIFEST.in

This file was deleted.

4 changes: 2 additions & 2 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
4 changes: 2 additions & 2 deletions pymap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 14 additions & 11 deletions pymap/backend/dict/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -243,35 +243,38 @@ 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
continue
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()
Expand Down
6 changes: 3 additions & 3 deletions pymap/backend/maildir/mailbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) \
Expand Down Expand Up @@ -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())
Expand Down
7 changes: 4 additions & 3 deletions pymap/backend/redis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions pymap/backend/redis/scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pymap/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = {}
Expand Down
8 changes: 5 additions & 3 deletions pymap/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion pymap/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 4 additions & 7 deletions pymap/parsing/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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]:
Expand Down
2 changes: 1 addition & 1 deletion pymap/parsing/specials/searchkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
16 changes: 6 additions & 10 deletions pymap/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
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.
PluginT = TypeVar('PluginT')


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
<https://packaging.python.org/guides/creating-and-discovering-plugins/#using-package-metadata>`_.
>>> example: Plugin[Example] = Plugin('plugins.example')
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit 6f197aa

Please sign in to comment.