diff --git a/CHANGES.rst b/CHANGES.rst index 942c725422..df861c3af9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -75,6 +75,10 @@ Storage (GITHUB-1431) [Tomaz Muraus] +- Add type annotations for the base storage API. + (GITHUB-1410) + [Clemens Wolff - @c-w] + DNS ~~~ @@ -84,10 +88,14 @@ DNS didn't contain ``priority`` key. Reported by James Montgomery - @gh-jamesmontgomery. - + (GITHUB-1428, GITHUB-1429) [Tomaz Muraus] +- Add type annotations for the base DNS API. + (GITHUB-1434) + [Tomaz Muraus] + Container ~~~~~~~~~ diff --git a/example_compute.py b/example_compute.py index 92b22021cf..f93c26aaf2 100644 --- a/example_compute.py +++ b/example_compute.py @@ -21,14 +21,8 @@ from typing import Type, cast -ec2_cls = get_driver(Provider.EC2) -rackspace_cls = get_driver(Provider.RACKSPACE) - -# NOTE: If you are using driver methods which are not part of the standard API, -# you need to explicitly cast the driver class reference to the correct class -# for type checking to work correctly -EC2 = cast(Type[EC2NodeDriver], ec2_cls) -Rackspace = cast(Type[RackspaceNodeDriver], rackspace_cls) +EC2 = get_driver(Provider.EC2) +Rackspace = get_driver(Provider.RACKSPACE) drivers = [EC2('access key id', 'secret key', region='us-east-1'), Rackspace('username', 'api key', region='iad')] diff --git a/libcloud/common/types.py b/libcloud/common/types.py index dcadfb3645..e73b295f02 100644 --- a/libcloud/common/types.py +++ b/libcloud/common/types.py @@ -15,14 +15,20 @@ from typing import Optional from typing import Callable +from typing import Union +from typing import cast + +from enum import Enum from libcloud.utils.py3 import httplib +from libcloud.utils.py3 import _real_unicode if False: # Work around for MYPY for cyclic import problem from libcloud.compute.base import BaseDriver __all__ = [ + "Type", "LibcloudError", "MalformedResponseError", "ProviderError", @@ -32,6 +38,60 @@ ] +class Type(Enum): + @classmethod + def tostring(cls, value): + # type: (Union[Enum, str]) -> str + """Return the string representation of the state object attribute + :param str value: the state object to turn into string + :return: the uppercase string that represents the state object + :rtype: str + """ + value = cast(Enum, value) + return str(value._value_).upper() + + @classmethod + def fromstring(cls, value): + # type: (str) -> str + """Return the state object attribute that matches the string + :param str value: the string to look up + :return: the state object attribute that matches the string + :rtype: str + """ + return getattr(cls, value.upper(), None) + + """ + NOTE: These methods are here for backward compatibility reasons where + Type values were simple strings and Type didn't inherit from Enum. + """ + + def __eq__(self, other): + if isinstance(other, Type): + return other.value == self.value + elif isinstance(other, (str, _real_unicode)): + return self.value == other + + return super(Type, self).__eq__(other) + + def upper(self): + return self.value.upper() # pylint: disable=no-member + + def lower(self): + return self.value.lower() # pylint: disable=no-member + + def __ne__(self, other): + return not self.__eq__(other) + + def __str__(self): + return str(self.value) + + def __repr__(self): + return self.value + + def __hash__(self): + return id(self) + + class LibcloudError(Exception): """The base class for other libcloud exceptions""" diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py index 48843dde33..c341cca96f 100644 --- a/libcloud/compute/types.py +++ b/libcloud/compute/types.py @@ -16,12 +16,7 @@ Base types used by other parts of libcloud """ -from typing import Union -from typing import cast - -from enum import Enum - -from libcloud.utils.py3 import _real_unicode +from libcloud.common.types import Type from libcloud.common.types import LibcloudError, MalformedResponseError from libcloud.common.types import InvalidCredsError, InvalidCredsException @@ -41,60 +36,6 @@ ] -class Type(Enum): - @classmethod - def tostring(cls, value): - # type: (Union[Enum, str]) -> str - """Return the string representation of the state object attribute - :param str value: the state object to turn into string - :return: the uppercase string that represents the state object - :rtype: str - """ - value = cast(Enum, value) - return str(value._value_).upper() - - @classmethod - def fromstring(cls, value): - # type: (str) -> str - """Return the state object attribute that matches the string - :param str value: the string to look up - :return: the state object attribute that matches the string - :rtype: str - """ - return getattr(cls, value.upper(), None) - - """ - NOTE: These methods are here for backward compatibility reasons where - Type values were simple strings and Type didn't inherit from Enum. - """ - - def __eq__(self, other): - if isinstance(other, Type): - return other.value == self.value - elif isinstance(other, (str, _real_unicode)): - return self.value == other - - return super(Type, self).__eq__(other) - - def upper(self): - return self.value.upper() # pylint: disable=no-member - - def lower(self): - return self.value.lower() # pylint: disable=no-member - - def __ne__(self, other): - return not self.__eq__(other) - - def __str__(self): - return str(self.value) - - def __repr__(self): - return self.value - - def __hash__(self): - return id(self) - - class Provider(Type): """ Defines for each of the supported providers diff --git a/libcloud/dns/base.py b/libcloud/dns/base.py index e963e2d430..95e2d2ad9e 100644 --- a/libcloud/dns/base.py +++ b/libcloud/dns/base.py @@ -15,9 +15,18 @@ from __future__ import with_statement +from typing import Dict +from typing import Iterator +from typing import List +from typing import Optional +from typing import Union +from typing import Type +from typing import Any + import datetime from libcloud import __version__ +from libcloud.common.base import Connection from libcloud.common.base import ConnectionUserAndKey, BaseDriver from libcloud.dns.types import RecordType @@ -33,7 +42,14 @@ class Zone(object): DNS zone. """ - def __init__(self, id, domain, type, ttl, driver, extra=None): + def __init__(self, + id, # type: str + domain, # type: str + type, # type: str + ttl, # type: int + driver, # type: DNSDriver + extra=None # type: dict + ): """ :param id: Zone id. :type id: ``str`` @@ -61,27 +77,39 @@ def __init__(self, id, domain, type, ttl, driver, extra=None): self.extra = extra or {} def list_records(self): + # type: () -> List[Record] return self.driver.list_records(zone=self) def create_record(self, name, type, data, extra=None): + # type: (str, RecordType, str, Optional[dict]) -> Record return self.driver.create_record(name=name, zone=self, type=type, data=data, extra=extra) - def update(self, domain=None, type=None, ttl=None, extra=None): + def update(self, + domain=None, # type: Optional[str] + type=None, # type: Optional[str] + ttl=None, # type: Optional[int] + extra=None # tyoe: Optional[dict] + ): + # type: (...) -> Zone return self.driver.update_zone(zone=self, domain=domain, type=type, ttl=ttl, extra=extra) def delete(self): + # type: () -> bool return self.driver.delete_zone(zone=self) def export_to_bind_format(self): + # type: () -> str return self.driver.export_zone_to_bind_format(zone=self) def export_to_bind_zone_file(self, file_path): + # type: (str) -> None self.driver.export_zone_to_bind_zone_file(zone=self, file_path=file_path) def __repr__(self): + # type: () -> str return ('' % (self.domain, self.ttl, self.driver.name)) @@ -91,8 +119,16 @@ class Record(object): Zone record / resource. """ - def __init__(self, id, name, type, data, zone, driver, ttl=None, - extra=None): + def __init__(self, + id, # type: str + name, # type: str + type, # type: RecordType + data, # type: str + zone, # type: Zone + driver, # type: DNSDriver + ttl=None, # type: int + extra=None # type: dict + ): """ :param id: Record id :type id: ``str`` @@ -127,22 +163,35 @@ def __init__(self, id, name, type, data, zone, driver, ttl=None, self.ttl = ttl self.extra = extra or {} - def update(self, name=None, type=None, data=None, extra=None): + def update(self, + name=None, # type: Optional[str] + type=None, # type: Optional[RecordType] + data=None, # type: Optional[str] + extra=None # type: Optional[dict] + ): + # type: (...) -> Record return self.driver.update_record(record=self, name=name, type=type, data=data, extra=extra) def delete(self): + # type: () -> bool return self.driver.delete_record(record=self) def _get_numeric_id(self): + # type: () -> Union[int, str] record_id = self.id + if not record_id: + return '' + if record_id.isdigit(): - record_id = int(record_id) + record_id_int = int(record_id) + return record_id_int return record_id def __repr__(self): + # type: () -> str zone = self.zone.domain if self.zone.domain else self.zone.id return ('' % @@ -156,15 +205,22 @@ class DNSDriver(BaseDriver): This class is always subclassed by a specific driver. """ - connectionCls = ConnectionUserAndKey - name = None - website = None + connectionCls = ConnectionUserAndKey # type: Type[Connection] + name = None # type: str + website = None # type: str # Map libcloud record type enum to provider record type name - RECORD_TYPE_MAP = {} - - def __init__(self, key, secret=None, secure=True, host=None, port=None, - **kwargs): + RECORD_TYPE_MAP = {} # type: Dict[RecordType, str] + + def __init__(self, + key, # type: str + secret=None, # type: Optional[str] + secure=True, # type: bool + host=None, # type: Optional[str] + port=None, # type: Optional[int] + **kwargs # type: Optional[Any] + ): + # type: (...) -> None """ :param key: API key or username to used (required) :type key: ``str`` @@ -188,6 +244,7 @@ def __init__(self, key, secret=None, secure=True, host=None, port=None, host=host, port=port, **kwargs) def list_record_types(self): + # type: () -> List[RecordType] """ Return a list of RecordType objects supported by the provider. @@ -196,6 +253,7 @@ def list_record_types(self): return list(self.RECORD_TYPE_MAP.keys()) def iterate_zones(self): + # type: () -> Iterator[Zone] """ Return a generator to iterate over available zones. @@ -205,6 +263,7 @@ def iterate_zones(self): 'iterate_zones not implemented for this driver') def list_zones(self): + # type: () -> List[Zone] """ Return a list of zones. @@ -213,6 +272,7 @@ def list_zones(self): return list(self.iterate_zones()) def iterate_records(self, zone): + # type: (Zone) -> Iterator[Record] """ Return a generator to iterate over records for the provided zone. @@ -225,6 +285,7 @@ def iterate_records(self, zone): 'iterate_records not implemented for this driver') def list_records(self, zone): + # type: (Zone) -> List[Record] """ Return a list of records for the provided zone. @@ -236,6 +297,7 @@ def list_records(self, zone): return list(self.iterate_records(zone)) def get_zone(self, zone_id): + # type: (str) -> Zone """ Return a Zone instance. @@ -248,6 +310,7 @@ def get_zone(self, zone_id): 'get_zone not implemented for this driver') def get_record(self, zone_id, record_id): + # type: (str, str) -> Record """ Return a Record instance. @@ -263,6 +326,7 @@ def get_record(self, zone_id, record_id): 'get_record not implemented for this driver') def create_zone(self, domain, type='master', ttl=None, extra=None): + # type: (str, str, Optional[int], Optional[dict]) -> Zone """ Create a new zone. @@ -283,7 +347,14 @@ def create_zone(self, domain, type='master', ttl=None, extra=None): raise NotImplementedError( 'create_zone not implemented for this driver') - def update_zone(self, zone, domain, type='master', ttl=None, extra=None): + def update_zone(self, + zone, # type: Zone + domain, # type: Optional[str] + type='master', # type: Optional[str] + ttl=None, # type: Optional[int] + extra=None # type: Optional[dict] + ): + # type: (...) -> Zone """ Update an existing zone. @@ -308,6 +379,7 @@ def update_zone(self, zone, domain, type='master', ttl=None, extra=None): 'update_zone not implemented for this driver') def create_record(self, name, zone, type, data, extra=None): + # type: (str, Zone, RecordType, str, Optional[dict]) -> Record """ Create a new record. @@ -334,7 +406,13 @@ def create_record(self, name, zone, type, data, extra=None): raise NotImplementedError( 'create_record not implemented for this driver') - def update_record(self, record, name, type, data, extra=None): + def update_record(self, + record, # type: Record + name, # type: Optional[str] + type, # type: Optional[RecordType] + data, # type: Optional[str] + extra=None # type: Optional[dict] + ): """ Update an existing record. @@ -362,6 +440,7 @@ def update_record(self, record, name, type, data, extra=None): 'update_record not implemented for this driver') def delete_zone(self, zone): + # type: (Zone) -> bool """ Delete a zone. @@ -376,6 +455,7 @@ def delete_zone(self, zone): 'delete_zone not implemented for this driver') def delete_record(self, record): + # type: (Record) -> bool """ Delete a record. @@ -388,6 +468,7 @@ def delete_record(self, record): 'delete_record not implemented for this driver') def export_zone_to_bind_format(self, zone): + # type: (Zone) -> str """ Export Zone object to the BIND compatible format. @@ -422,6 +503,7 @@ def export_zone_to_bind_format(self, zone): return output def export_zone_to_bind_zone_file(self, zone, file_path): + # type: (Zone, str) -> None """ Export Zone object to the BIND compatible format and write result to a file. @@ -438,6 +520,7 @@ def export_zone_to_bind_zone_file(self, zone, file_path): fp.write(result) def _get_bind_record_line(self, record): + # type: (Record) -> str """ Generate BIND record line for the provided record. @@ -447,7 +530,7 @@ def _get_bind_record_line(self, record): :return: Bind compatible record line. :rtype: ``str`` """ - parts = [] + parts = [] # type: List[Any] if record.name: name = '%(name)s.%(domain)s' % {'name': record.name, @@ -484,6 +567,7 @@ def _get_bind_record_line(self, record): return line def _string_to_record_type(self, string): + # type: (str) -> RecordType """ Return a string representation of a DNS record type to a libcloud RecordType ENUM. diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py index cc1409ce3e..bbd7db9033 100644 --- a/libcloud/dns/providers.py +++ b/libcloud/dns/providers.py @@ -13,11 +13,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Type +from typing import Union +from types import ModuleType +from typing import TYPE_CHECKING + from libcloud.dns.types import Provider from libcloud.dns.types import OLD_CONSTANT_TO_NEW_MAPPING from libcloud.common.providers import get_driver as _get_provider_driver from libcloud.common.providers import set_driver as _set_provider_driver + +if TYPE_CHECKING: + # NOTE: This is needed to avoid having setup.py depend on requests + from libcloud.dns.base import DNSDriver + + __all__ = [ 'DRIVERS', @@ -91,11 +102,13 @@ def get_driver(provider): + # type: (Union[Provider, str]) -> Type[DNSDriver] deprecated_constants = OLD_CONSTANT_TO_NEW_MAPPING return _get_provider_driver(drivers=DRIVERS, provider=provider, deprecated_constants=deprecated_constants) def set_driver(provider, module, klass): + # type: (Union[Provider, str], ModuleType, type) -> Type[DNSDriver] return _set_provider_driver(drivers=DRIVERS, provider=provider, module=module, klass=klass) diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py index 72a6dbf4db..a06bc844d4 100644 --- a/libcloud/dns/types.py +++ b/libcloud/dns/types.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from libcloud.common.types import Type from libcloud.common.types import LibcloudError __all__ = [ @@ -76,7 +77,7 @@ class Provider(object): } -class RecordType(object): +class RecordType(Type): """ DNS record type. """ diff --git a/mypy.ini b/mypy.ini index 335c14773d..c03b4187fc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -34,13 +34,9 @@ ignore_errors = True ignore_errors = True # NOTE: Fixing drivers will take a while -[mypy-libcloud.storage.drivers.s3] +[mypy-libcloud.storage.drivers.*] ignore_errors = True # NOTE: Fixing drivers will take a while -[mypy-libcloud.storage.drivers.google_storage] -ignore_errors = True - -# NOTE: Fixing drivers will take a while -[mypy-libcloud.storage.drivers.cloudfiles] +[mypy-libcloud.dns.drivers.*] ignore_errors = True diff --git a/tox.ini b/tox.ini index 076f154072..56cf5b1ac9 100644 --- a/tox.ini +++ b/tox.ini @@ -199,6 +199,7 @@ commands = mypy --no-incremental libcloud/common/ mypy --no-incremental libcloud/compute/ mypy --no-incremental libcloud/storage/ + mypy --no-incremental libcloud/dns/ mypy --no-incremental example_compute.py mypy --no-incremental example_dns.py mypy --no-incremental example_container.py