Skip to content

Commit 8886854

Browse files
author
Hugo Osvaldo Barrera
authored
Merge pull request #912 from pimutils/typing
Add some typing hints
2 parents 6af4dd1 + f3714fc commit 8886854

File tree

11 files changed

+114
-55
lines changed

11 files changed

+114
-55
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,13 @@ repos:
2222
hooks:
2323
- id: isort
2424
name: isort (python)
25+
- repo: https://github.com/pre-commit/mirrors-mypy
26+
rev: "v0.910"
27+
hooks:
28+
- id: mypy
29+
files: vdirsyncer/.*
30+
additional_dependencies:
31+
- types-setuptools
32+
- types-docutils
33+
- types-requests
34+
- types-atomicwrites

setup.cfg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,8 @@ import-order-style = smarkets
2424

2525
[isort]
2626
force_single_line=true
27+
28+
[mypy]
29+
ignore_missing_imports = True
30+
# See https://github.com/python/mypy/issues/7511:
31+
warn_no_return = False

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
class PrintRequirements(Command):
2727
description = "Prints minimal requirements"
28-
user_options = []
28+
user_options: list = []
2929

3030
def initialize_options(self):
3131
pass

tests/system/cli/test_discover.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from textwrap import dedent
3+
from typing import List
34

45
import pytest
56

@@ -208,6 +209,12 @@ def __init__(self, require_collection, **kw):
208209
assert not kw.get("collection")
209210
raise exceptions.CollectionRequired()
210211

212+
async def get(self, href: str):
213+
raise NotImplementedError()
214+
215+
async def list(self) -> List[tuple]:
216+
raise NotImplementedError()
217+
211218
from vdirsyncer.cli.utils import storage_names
212219

213220
monkeypatch.setitem(storage_names._storages, "test", TestStorage)

vdirsyncer/storage/base.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import contextlib
22
import functools
3+
from abc import ABCMeta
4+
from abc import abstractmethod
5+
from typing import Iterable
6+
from typing import List
37
from typing import Optional
48

9+
from vdirsyncer.vobject import Item
10+
511
from .. import exceptions
612
from ..utils import uniq
713

814

915
def mutating_storage_method(f):
16+
"""Wrap a method and fail if the instance is readonly."""
17+
1018
@functools.wraps(f)
1119
async def inner(self, *args, **kwargs):
1220
if self.read_only:
@@ -16,8 +24,10 @@ async def inner(self, *args, **kwargs):
1624
return inner
1725

1826

19-
class StorageMeta(type):
27+
class StorageMeta(ABCMeta):
2028
def __init__(cls, name, bases, d):
29+
"""Wrap mutating methods to fail if the storage is readonly."""
30+
2131
for method in ("update", "upload", "delete"):
2232
setattr(cls, method, mutating_storage_method(getattr(cls, method)))
2333
return super().__init__(name, bases, d)
@@ -48,7 +58,7 @@ class Storage(metaclass=StorageMeta):
4858

4959
# The string used in the config to denote the type of storage. Should be
5060
# overridden by subclasses.
51-
storage_name = None
61+
storage_name: str
5262

5363
# The string used in the config to denote a particular instance. Will be
5464
# overridden during instantiation.
@@ -63,7 +73,7 @@ class Storage(metaclass=StorageMeta):
6373
read_only = False
6474

6575
# The attribute values to show in the representation of the storage.
66-
_repr_attributes = ()
76+
_repr_attributes: List[str] = []
6777

6878
def __init__(self, instance_name=None, read_only=None, collection=None):
6979
if read_only is None:
@@ -121,23 +131,23 @@ def __repr__(self):
121131
{x: getattr(self, x) for x in self._repr_attributes},
122132
)
123133

124-
async def list(self):
134+
@abstractmethod
135+
async def list(self) -> List[tuple]:
125136
"""
126137
:returns: list of (href, etag)
127138
"""
128-
raise NotImplementedError()
129139

130-
async def get(self, href):
140+
@abstractmethod
141+
async def get(self, href: str):
131142
"""Fetch a single item.
132143
133144
:param href: href to fetch
134145
:returns: (item, etag)
135146
:raises: :exc:`vdirsyncer.exceptions.PreconditionFailed` if item can't
136147
be found.
137148
"""
138-
raise NotImplementedError()
139149

140-
async def get_multi(self, hrefs):
150+
async def get_multi(self, hrefs: Iterable[str]):
141151
"""Fetch multiple items. Duplicate hrefs must be ignored.
142152
143153
Functionally similar to :py:meth:`get`, but might bring performance
@@ -152,19 +162,16 @@ async def get_multi(self, hrefs):
152162
item, etag = await self.get(href)
153163
yield href, item, etag
154164

155-
async def has(self, href):
156-
"""Check if an item exists by its href.
157-
158-
:returns: True or False
159-
"""
165+
async def has(self, href) -> bool:
166+
"""Check if an item exists by its href."""
160167
try:
161168
await self.get(href)
162169
except exceptions.PreconditionFailed:
163170
return False
164171
else:
165172
return True
166173

167-
async def upload(self, item):
174+
async def upload(self, item: Item):
168175
"""Upload a new item.
169176
170177
In cases where the new etag cannot be atomically determined (i.e. in
@@ -179,7 +186,7 @@ async def upload(self, item):
179186
"""
180187
raise NotImplementedError()
181188

182-
async def update(self, href, item, etag):
189+
async def update(self, href: str, item: Item, etag):
183190
"""Update an item.
184191
185192
The etag may be none in some cases, see `upload`.
@@ -192,7 +199,7 @@ async def update(self, href, item, etag):
192199
"""
193200
raise NotImplementedError()
194201

195-
async def delete(self, href, etag):
202+
async def delete(self, href: str, etag: str):
196203
"""Delete an item by href.
197204
198205
:raises: :exc:`vdirsyncer.exceptions.PreconditionFailed` when item has
@@ -228,21 +235,19 @@ async def get_meta(self, key: str) -> Optional[str]:
228235
:param key: The metadata key.
229236
:return: The metadata or None, if metadata is missing.
230237
"""
231-
232238
raise NotImplementedError("This storage does not support metadata.")
233239

234240
async def set_meta(self, key: str, value: Optional[str]):
235-
"""Get metadata value for collection/storage.
241+
"""Set metadata value for collection/storage.
236242
237243
:param key: The metadata key.
238244
:param value: The value. Use None to delete the data.
239245
"""
240-
241246
raise NotImplementedError("This storage does not support metadata.")
242247

243248

244249
def normalize_meta_value(value) -> Optional[str]:
245250
# `None` is returned by iCloud for empty properties.
246251
if value is None or value == "None":
247-
return
252+
return None
248253
return value.strip() if value else ""

vdirsyncer/storage/dav.py

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
import logging
33
import urllib.parse as urlparse
44
import xml.etree.ElementTree as etree
5+
from abc import abstractmethod
56
from inspect import getfullargspec
67
from inspect import signature
78
from typing import Optional
9+
from typing import Type
810

911
import aiohttp
1012
import aiostream
1113

1214
from vdirsyncer.exceptions import Error
15+
from vdirsyncer.vobject import Item
1316

1417
from .. import exceptions
1518
from .. import http
@@ -18,7 +21,6 @@
1821
from ..http import prepare_auth
1922
from ..http import prepare_client_cert
2023
from ..http import prepare_verify
21-
from ..vobject import Item
2224
from .base import Storage
2325
from .base import normalize_meta_value
2426

@@ -146,11 +148,31 @@ def _fuzzy_matches_mimetype(strict, weak):
146148

147149

148150
class Discover:
149-
_namespace = None
150-
_resourcetype = None
151-
_homeset_xml = None
152-
_homeset_tag = None
153-
_well_known_uri = None
151+
@property
152+
@abstractmethod
153+
def _namespace(self) -> str:
154+
pass
155+
156+
@property
157+
@abstractmethod
158+
def _resourcetype(self) -> Optional[str]:
159+
pass
160+
161+
@property
162+
@abstractmethod
163+
def _homeset_xml(self) -> bytes:
164+
pass
165+
166+
@property
167+
@abstractmethod
168+
def _homeset_tag(self) -> str:
169+
pass
170+
171+
@property
172+
@abstractmethod
173+
def _well_known_uri(self) -> str:
174+
pass
175+
154176
_collection_xml = b"""
155177
<propfind xmlns="DAV:">
156178
<prop>
@@ -347,7 +369,7 @@ class CalDiscover(Discover):
347369

348370
class CardDiscover(Discover):
349371
_namespace = "urn:ietf:params:xml:ns:carddav"
350-
_resourcetype = "{%s}addressbook" % _namespace
372+
_resourcetype: Optional[str] = "{%s}addressbook" % _namespace
351373
_homeset_xml = b"""
352374
<propfind xmlns="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav">
353375
<prop>
@@ -434,21 +456,31 @@ def get_default_headers(self):
434456

435457
class DAVStorage(Storage):
436458
# the file extension of items. Useful for testing against radicale.
437-
fileext = None
459+
fileext: str
438460
# mimetype of items
439-
item_mimetype = None
440-
# XML to use when fetching multiple hrefs.
441-
get_multi_template = None
442-
# The LXML query for extracting results in get_multi
443-
get_multi_data_query = None
444-
# The Discover subclass to use
445-
discovery_class = None
461+
item_mimetype: str
462+
463+
@property
464+
@abstractmethod
465+
def get_multi_template(self) -> str:
466+
"""XML to use when fetching multiple hrefs."""
467+
468+
@property
469+
@abstractmethod
470+
def get_multi_data_query(self) -> str:
471+
"""LXML query for extracting results in get_multi."""
472+
473+
@property
474+
@abstractmethod
475+
def discovery_class(self) -> Type[Discover]:
476+
"""Discover subclass to use."""
477+
446478
# The DAVSession class to use
447479
session_class = DAVSession
448480

449481
connector: aiohttp.TCPConnector
450482

451-
_repr_attributes = ("username", "url")
483+
_repr_attributes = ["username", "url"]
452484

453485
_property_table = {
454486
"displayname": ("displayname", "DAV:"),
@@ -466,7 +498,8 @@ def __init__(self, *, connector, **kwargs):
466498
)
467499
super().__init__(**kwargs)
468500

469-
__init__.__signature__ = signature(session_class.__init__)
501+
__init__.__signature__ = signature(session_class.__init__) # type: ignore
502+
# See https://github.com/python/mypy/issues/5958
470503

471504
@classmethod
472505
async def discover(cls, **kwargs):
@@ -492,7 +525,7 @@ def _get_href(self, item):
492525
def _is_item_mimetype(self, mimetype):
493526
return _fuzzy_matches_mimetype(self.item_mimetype, mimetype)
494527

495-
async def get(self, href):
528+
async def get(self, href: str):
496529
((actual_href, item, etag),) = await aiostream.stream.list(
497530
self.get_multi([href])
498531
)
@@ -588,7 +621,7 @@ async def update(self, href, item, etag):
588621
href, etag = await self._put(self._normalize_href(href), item, etag)
589622
return etag
590623

591-
async def upload(self, item):
624+
async def upload(self, item: Item):
592625
href = self._get_href(item)
593626
rv = await self._put(href, item, None)
594627
return rv
@@ -687,17 +720,14 @@ async def get_meta(self, key) -> Optional[str]:
687720
raise exceptions.UnsupportedMetadataError()
688721

689722
xpath = f"{{{namespace}}}{tagname}"
690-
data = """<?xml version="1.0" encoding="utf-8" ?>
723+
body = f"""<?xml version="1.0" encoding="utf-8" ?>
691724
<propfind xmlns="DAV:">
692725
<prop>
693-
{}
726+
{etree.tostring(etree.Element(xpath), encoding="unicode")}
694727
</prop>
695728
</propfind>
696-
""".format(
697-
etree.tostring(etree.Element(xpath), encoding="unicode")
698-
).encode(
699-
"utf-8"
700-
)
729+
"""
730+
data = body.encode("utf-8")
701731

702732
headers = self.session.get_default_headers()
703733
headers["Depth"] = "0"

vdirsyncer/storage/filesystem.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
class FilesystemStorage(Storage):
2121

2222
storage_name = "filesystem"
23-
_repr_attributes = ("path",)
23+
_repr_attributes = ["path"]
2424

2525
def __init__(
2626
self,

0 commit comments

Comments
 (0)