Przeglądaj źródła

Add comprehensive typing to the public interface + mypy tox check

Strongly type CloudBridge's public API so downstream users get a
well-typed interface, even though the internal providers remain untyped.
Because the interface layer expresses the entire return-type web
(create_provider() -> CloudProvider -> ComputeService -> InstanceService
-> ResultList[Instance] -> Instance), annotating it plus shipping a PEP
561 py.typed marker makes the whole API typed for consumers regardless of
provider internals.

- Annotate the full interface layer (interfaces/{resources,services,
  subservices,provider,exceptions}.py), factory.py and __init__.py.
- Make PageableObjectMixin and ResultList generic so list()/iteration are
  typed (ResultList[Instance], etc.); add `from __future__ import
  annotations` and TYPE_CHECKING blocks to break import cycles.
- Ship cloudbridge/py.typed via [tool.setuptools.package-data].
- Add a strict [tool.mypy] baseline over the whole package, with base.*
  and providers.* temporarily exempted (ignore_errors) as a gradual
  ratchet -- remove a module from that list as it gets typed.
- Add a lean `mypy` tox env (skip_install) to envlist and run it in the
  CI lint job; add mypy>=1.11 to the dev extra.

Interface behaviour is unchanged: @abstractproperty / __metaclass__ are
kept as-is (no modernization), so runtime abstractness of the untyped
providers is unaffected.
Nuwan Goonasekera 1 dzień temu
rodzic
commit
bcd6d79171

+ 3 - 0
.github/workflows/integration.yaml

@@ -60,6 +60,9 @@ jobs:
       - name: Run tox
         run: tox -e lint
 
+      - name: Run mypy
+        run: tox -e mypy
+
   mock:
     name: Mock-provider tests
     runs-on: ubuntu-latest

+ 8 - 5
cloudbridge/__init__.py

@@ -1,11 +1,12 @@
 """Library setup."""
 import logging
+from typing import Any
 
 # Current version of the library
 __version__ = '4.1.0'
 
 
-def get_version():
+def get_version() -> str:
     """
     Return a string with the current version of the library.
 
@@ -15,7 +16,7 @@ def get_version():
     return __version__
 
 
-def init_logging():
+def init_logging() -> None:
     """
     Initialize logging for testing.
 
@@ -35,7 +36,7 @@ class CBLogger(logging.Logger):
     Add a ``trace`` log level, numeric value 5: ``log.trace("Log message")``
     """
 
-    def trace(self, msg, *args, **kwargs):
+    def trace(self, msg: object, *args: object, **kwargs: Any) -> None:
         """Add ``trace`` log level."""
         self.log(TRACE, msg, *args, **kwargs)
 
@@ -61,7 +62,8 @@ log.addHandler(logging.NullHandler())
 #   cloudbridge.set_file_logger(__name__, '/tmp/log')
 
 
-def set_stream_logger(name, level=TRACE, format_string=None):
+def set_stream_logger(name: str, level: int = TRACE,
+                      format_string: str | None = None) -> None:
     """A convenience method to set the global logger to stream."""
     global log
     if not format_string:
@@ -76,7 +78,8 @@ def set_stream_logger(name, level=TRACE, format_string=None):
     log = logger
 
 
-def set_file_logger(name, filepath, level=logging.INFO, format_string=None):
+def set_file_logger(name: str, filepath: str, level: int = logging.INFO,
+                    format_string: str | None = None) -> None:
     """A convenience method to set the global logger to a file."""
     global log
     if not format_string:

+ 15 - 11
cloudbridge/factory.py

@@ -3,6 +3,8 @@ import inspect
 import logging
 import pkgutil
 from collections import defaultdict
+from typing import Any
+from typing import cast
 
 from cloudbridge import providers
 from cloudbridge.interfaces import CloudProvider
@@ -26,11 +28,11 @@ class CloudProviderFactory(object):
     Get info and handle on the available cloud provider implementations.
     """
 
-    def __init__(self):
-        self.provider_list = defaultdict(dict)
+    def __init__(self) -> None:
+        self.provider_list: defaultdict[str, dict[str, Any]] = defaultdict(dict)
         log.debug("Providers List: %s", self.provider_list)
 
-    def register_provider_class(self, cls):
+    def register_provider_class(self, cls: type) -> None:
         """
         Registers a provider class with the factory. The class must
         inherit from cloudbridge.interfaces.CloudProvider
@@ -61,7 +63,7 @@ class CloudProviderFactory(object):
             log.debug("Class: %s does not implement the CloudProvider"
                       "  interface. Ignoring...", cls)
 
-    def discover_providers(self):
+    def discover_providers(self) -> None:
         """
         Discover all available providers within the
         ``cloudbridge.providers`` package.
@@ -74,7 +76,7 @@ class CloudProviderFactory(object):
             except Exception as e:
                 log.debug("Could not import provider: %s", e)
 
-    def _import_provider(self, module_name):
+    def _import_provider(self, module_name: str) -> None:
         """
         Imports and registers providers from the given module name.
         Raises an ImportError if the import does not succeed.
@@ -88,7 +90,7 @@ class CloudProviderFactory(object):
             log.debug("Registering the provider: %s", cls)
             self.register_provider_class(cls)
 
-    def list_providers(self):
+    def list_providers(self) -> dict[str, dict[str, Any]]:
         """
         Get a list of available providers.
 
@@ -108,7 +110,8 @@ class CloudProviderFactory(object):
         log.debug("List of available providers: %s", self.provider_list)
         return self.provider_list
 
-    def create_provider(self, name, config):
+    def create_provider(self, name: str,
+                        config: dict[str, Any]) -> CloudProvider:
         """
         Searches all available providers for a CloudProvider interface with the
         given name, and instantiates it based on the given config dictionary,
@@ -138,7 +141,7 @@ class CloudProviderFactory(object):
         log.debug("Created '%s' provider", name)
         return provider_class(config)
 
-    def get_provider_class(self, name):
+    def get_provider_class(self, name: str) -> type[CloudProvider] | None:
         """
         Return a class for the requested provider.
 
@@ -150,12 +153,13 @@ class CloudProviderFactory(object):
         impl = self.list_providers().get(name)
         if impl:
             log.debug("Returning provider class for %s", name)
-            return impl["class"]
+            return cast("type[CloudProvider]", impl["class"])
         else:
             log.debug("Provider with the name: %s not found", name)
             return None
 
-    def get_all_provider_classes(self, ignore_mocks=False):
+    def get_all_provider_classes(
+            self, ignore_mocks: bool = False) -> list[type[CloudProvider]]:
         """
         Returns a list of classes for all available provider implementations
 
@@ -167,7 +171,7 @@ class CloudProviderFactory(object):
         :return: A list of all available provider classes or an empty list
         if none found.
         """
-        all_providers = []
+        all_providers: list[type[CloudProvider]] = []
         for impl in self.list_providers().values():
             if ignore_mocks:
                 if not issubclass(impl["class"], TestMockHelperMixin):

+ 4 - 4
cloudbridge/interfaces/exceptions.py

@@ -54,7 +54,7 @@ class InvalidNameException(CloudBridgeBaseException):
     letters, which are not allowed in a resource name.
     """
 
-    def __init__(self, msg):
+    def __init__(self, msg: str) -> None:
         super(InvalidNameException, self).__init__(msg)
 
 
@@ -68,7 +68,7 @@ class InvalidLabelException(InvalidNameException):
     identical.
     """
 
-    def __init__(self, msg):
+    def __init__(self, msg: str) -> None:
         super(InvalidLabelException, self).__init__(msg)
 
 
@@ -79,7 +79,7 @@ class InvalidValueException(CloudBridgeBaseException):
     direction of a firewall rule other than TrafficDirection.INBOUND or
     TrafficDirection.OUTBOUND.
     """
-    def __init__(self, param, value):
+    def __init__(self, param: str, value: object) -> None:
         super(InvalidValueException, self).__init__(
             "Param %s has been given an unrecognised value %s" %
             (param, value))
@@ -100,5 +100,5 @@ class InvalidParamException(InvalidNameException):
     to a service.find() method.
     """
 
-    def __init__(self, msg):
+    def __init__(self, msg: str) -> None:
         super(InvalidParamException, self).__init__(msg)

+ 33 - 18
cloudbridge/interfaces/provider.py

@@ -1,9 +1,24 @@
 """
 Specification for a provider interface
 """
+from __future__ import annotations
+
 from abc import ABCMeta
 from abc import abstractmethod
 from abc import abstractproperty
+from typing import Any
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pyeventsystem.middleware import MiddlewareManager
+
+    from cloudbridge.interfaces.resources import Configuration
+    from cloudbridge.interfaces.resources import PlacementZone
+    from cloudbridge.interfaces.services import ComputeService
+    from cloudbridge.interfaces.services import DnsService
+    from cloudbridge.interfaces.services import NetworkingService
+    from cloudbridge.interfaces.services import SecurityService
+    from cloudbridge.interfaces.services import StorageService
 
 
 class CloudProvider(object):
@@ -13,7 +28,7 @@ class CloudProvider(object):
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def __init__(self, config):
+    def __init__(self, config: dict[str, Any]) -> None:
         """
         Create a new provider instance given a dictionary of
         configuration attributes.
@@ -31,7 +46,7 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def config(self):
+    def config(self) -> Configuration:
         """
         Returns the config object associated with this provider. This object
         is a subclass of :class:`dict` and will contain the properties
@@ -58,7 +73,7 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def middleware(self):
+    def middleware(self) -> MiddlewareManager:
         """
         Returns the middleware manager associated with this provider. The
         middleware manager can be used to add or remove middleware from
@@ -72,7 +87,7 @@ class CloudProvider(object):
         pass
 
     @abstractmethod
-    def clone(self, zone=None):
+    def clone(self, zone: PlacementZone | None = None) -> CloudProvider:
         """
         Create a clone of this provider. An optional `zone` parameter can be
         used to clone the provider to use a different zone.
@@ -101,7 +116,7 @@ class CloudProvider(object):
         pass
 
     @abstractmethod
-    def authenticate(self):
+    def authenticate(self) -> bool:
         """
         Checks whether a provider can be successfully authenticated with the
         configured settings. Clients are *not* required to call this method
@@ -127,7 +142,7 @@ class CloudProvider(object):
         pass
 
     @abstractmethod
-    def has_service(self, service_type):
+    def has_service(self, service_type: str) -> bool:
         """
         Checks whether this provider supports a given service.
 
@@ -149,7 +164,7 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def region_name(self):
+    def region_name(self) -> str:
         """
         Returns the region that this provider is connected to.
         All provider operations will take place within this region.
@@ -160,7 +175,7 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def zone_name(self):
+    def zone_name(self) -> str | None:
         """
         Returns the placement zone that this provider is connected to.
         All provider operations will take place within this zone. Placement
@@ -183,7 +198,7 @@ class CloudProvider(object):
 #         pass
 
     @abstractproperty
-    def compute(self):
+    def compute(self) -> ComputeService:
         """
         Provides access to all compute related services in this provider.
 
@@ -206,7 +221,7 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def networking(self):
+    def networking(self) -> NetworkingService:
         """
         Provide access to all network related services in this provider.
 
@@ -223,7 +238,7 @@ class CloudProvider(object):
         """
 
     @abstractproperty
-    def security(self):
+    def security(self) -> SecurityService:
         """
         Provides access to key pair management and firewall control
 
@@ -241,7 +256,7 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def storage(self):
+    def storage(self) -> StorageService:
         """
         Provides access to storage related services in this provider.
         This includes the volume, snapshot and bucket services,
@@ -262,7 +277,7 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def dns(self):
+    def dns(self) -> DnsService:
         """
         Provides access to all DNS related services.
 
@@ -288,14 +303,14 @@ class TestMockHelperMixin(object):
     like HTTPretty which take over socket communications.
     """
 
-    def setUpMock(self):
+    def setUpMock(self) -> None:
         """
         Called before a test is started.
         """
         raise NotImplementedError(
             'TestMockHelperMixin.setUpMock not implemented')
 
-    def tearDownMock(self):
+    def tearDownMock(self) -> None:
         """
         Called before test teardown.
         """
@@ -312,11 +327,11 @@ class ContainerProvider(object):
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def create_container(self):
+    def create_container(self) -> Any:
         pass
 
     @abstractmethod
-    def delete_container(self):
+    def delete_container(self) -> Any:
         pass
 
 
@@ -328,7 +343,7 @@ class DeploymentProvider(object):
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def deploy(self, target):
+    def deploy(self, target: Any) -> Any:
         """
         Deploys on given target, where target is an Instance or Container
         """

Plik diff jest za duży
+ 144 - 116
cloudbridge/interfaces/resources.py


Plik diff jest za duży
+ 183 - 106
cloudbridge/interfaces/services.py


+ 57 - 34
cloudbridge/interfaces/subservices.py

@@ -1,17 +1,31 @@
+from __future__ import annotations
+
+import builtins
 from abc import ABCMeta
 from abc import abstractmethod
+from typing import Any
 
+from cloudbridge.interfaces.resources import BucketObject
+from cloudbridge.interfaces.resources import DnsRecord
+from cloudbridge.interfaces.resources import FloatingIP
+from cloudbridge.interfaces.resources import Gateway
+from cloudbridge.interfaces.resources import InternetGateway
 from cloudbridge.interfaces.resources import PageableObjectMixin
+from cloudbridge.interfaces.resources import ResultList
+from cloudbridge.interfaces.resources import Subnet
+from cloudbridge.interfaces.resources import TrafficDirection
+from cloudbridge.interfaces.resources import VMFirewall
+from cloudbridge.interfaces.resources import VMFirewallRule
 
 
-class BucketObjectSubService(PageableObjectMixin):
+class BucketObjectSubService(PageableObjectMixin[BucketObject]):
     """
     A container service for objects within a bucket.
     """
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, name):
+    def get(self, name: str) -> BucketObject | None:
         """
         Retrieve a given object from this bucket.
 
@@ -25,7 +39,8 @@ class BucketObjectSubService(PageableObjectMixin):
 
     @abstractmethod
     # pylint:disable=arguments-differ
-    def list(self, limit=None, marker=None, prefix=None):
+    def list(self, limit: int | None = None, marker: str | None = None,
+             prefix: str | None = None) -> ResultList[BucketObject]:
         """
         List objects in this bucket.
 
@@ -44,7 +59,7 @@ class BucketObjectSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[BucketObject]:
         """
         Search for an object by a given list of attributes.
 
@@ -62,7 +77,7 @@ class BucketObjectSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def create(self, name):
+    def create(self, name: str) -> BucketObject:
         """
         Create a new object within this bucket.
 
@@ -72,14 +87,14 @@ class BucketObjectSubService(PageableObjectMixin):
         pass
 
 
-class GatewaySubService(PageableObjectMixin):
+class GatewaySubService(PageableObjectMixin[InternetGateway]):
     """
     Manage internet gateway resources.
     """
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get_or_create(self):
+    def get_or_create(self) -> InternetGateway:
         """
         Creates new or returns an existing internet gateway for a network.
 
@@ -92,7 +107,7 @@ class GatewaySubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def delete(self, gateway):
+    def delete(self, gateway: Gateway) -> None:
         """
         Delete a gateway.
 
@@ -102,7 +117,8 @@ class GatewaySubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[InternetGateway]:
         """
         List all available internet gateways.
 
@@ -112,14 +128,14 @@ class GatewaySubService(PageableObjectMixin):
         pass
 
 
-class FloatingIPSubService(PageableObjectMixin):
+class FloatingIPSubService(PageableObjectMixin[FloatingIP]):
     """
     Base interface for a FloatingIP Service.
     """
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, fip_id):
+    def get(self, fip_id: str) -> FloatingIP | None:
         """
         Returns a FloatingIP given its ID or ``None`` if not found.
 
@@ -132,7 +148,8 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[FloatingIP]:
         """
         List floating (i.e., static) IP addresses.
 
@@ -142,7 +159,7 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[FloatingIP]:
         """
         Searches for a FloatingIP by a given list of attributes.
 
@@ -162,7 +179,7 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def create(self):
+    def create(self) -> FloatingIP:
         """
         Allocate a new floating (i.e., static) IP address.
 
@@ -172,7 +189,7 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def delete(self, fip_id):
+    def delete(self, fip_id: FloatingIP | str) -> None:
         """
         Delete an existing FloatingIP.
 
@@ -182,14 +199,14 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
 
 
-class VMFirewallRuleSubService(PageableObjectMixin):
+class VMFirewallRuleSubService(PageableObjectMixin[VMFirewallRule]):
     """
     Base interface for Firewall rules.
     """
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, rule_id):
+    def get(self, rule_id: str) -> VMFirewallRule | None:
         """
         Return a firewall rule given its ID.
 
@@ -212,7 +229,8 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[VMFirewallRule]:
         """
         List all firewall rules associated with this firewall.
 
@@ -222,8 +240,10 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def create(self, direction, protocol=None, from_port=None,
-               to_port=None, cidr=None, src_dest_fw=None):
+    def create(self, direction: TrafficDirection, protocol: str | None = None,
+               from_port: int | None = None, to_port: int | None = None,
+               cidr: str | builtins.list[str] | None = None,
+               src_dest_fw: VMFirewall | None = None) -> VMFirewallRule:
         """
         Create a VM firewall rule.
 
@@ -274,7 +294,7 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[VMFirewallRule]:
         """
         Find a firewall rule filtered by the given parameters.
 
@@ -310,7 +330,7 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def delete(self, rule_id):
+    def delete(self, rule_id: str) -> None:
         """
         Delete an existing VMFirewall rule.
 
@@ -320,14 +340,14 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
 
 
-class SubnetSubService(PageableObjectMixin):
+class SubnetSubService(PageableObjectMixin[Subnet]):
     """
     Base interface for a Subnet Service.
     """
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, subnet_id):
+    def get(self, subnet_id: str) -> Subnet | None:
         """
         Returns a Subnet given its ID or ``None`` if not found.
 
@@ -340,7 +360,8 @@ class SubnetSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[Subnet]:
         """
         List subnets within the network holding this subservice.
 
@@ -350,7 +371,7 @@ class SubnetSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[Subnet]:
         """
         Searches for a Subnet by a given list of attributes.
 
@@ -370,7 +391,7 @@ class SubnetSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def create(self, label, cidr_block):
+    def create(self, label: str, cidr_block: str) -> Subnet:
         """
         Create a new subnet within the network holding this subservice.
 
@@ -387,7 +408,7 @@ class SubnetSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def delete(self, subnet_id):
+    def delete(self, subnet_id: Subnet | str) -> None:
         """
         Delete an existing Subnet.
 
@@ -397,14 +418,14 @@ class SubnetSubService(PageableObjectMixin):
         pass
 
 
-class DnsRecordSubService(PageableObjectMixin):
+class DnsRecordSubService(PageableObjectMixin[DnsRecord]):
     """
     Base interface for a Dns Record Service.
     """
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, record_id):
+    def get(self, record_id: str) -> DnsRecord | None:
         """
         Returns a Dns Record given its ID or ``None`` if not found.
 
@@ -417,7 +438,8 @@ class DnsRecordSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[DnsRecord]:
         """
         List Dns Records within the Dns Zone holding this subservice.
 
@@ -427,7 +449,7 @@ class DnsRecordSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[DnsRecord]:
         """
         Searches for a DnsRecord by a given list of attributes.
 
@@ -447,7 +469,8 @@ class DnsRecordSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def create(self, label, type, data, ttl=None):
+    def create(self, label: str, type: str, data: str,
+               ttl: int | None = None) -> DnsRecord:
         """
         Create a new DnsRecord within the Dns Zone holding this subservice.
 
@@ -469,7 +492,7 @@ class DnsRecordSubService(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def delete(self, record_id):
+    def delete(self, record_id: DnsRecord | str) -> None:
         """
         Delete an existing DnsRecord.
 

+ 0 - 0
cloudbridge/py.typed


+ 30 - 0
pyproject.toml

@@ -91,6 +91,7 @@ dev = [
     "pydevd",
     "flake8>=3.3.0",
     "flake8-import-order>=0.12",
+    "mypy>=1.11",
 ]
 
 [tool.setuptools.dynamic]
@@ -100,6 +101,10 @@ version = { attr = "cloudbridge.__version__" }
 include = ["cloudbridge*"]
 exclude = ["tests*"]
 
+[tool.setuptools.package-data]
+# Ship the PEP 561 marker so downstream consumers pick up our type hints.
+cloudbridge = ["py.typed"]
+
 [tool.coverage.run]
 branch = true
 source = ["cloudbridge"]
@@ -108,3 +113,28 @@ omit = [
     "cloudbridge/__init__.py",
 ]
 parallel = true
+
+[tool.mypy]
+# CloudBridge is typed gradually. The public API (the interface layer and the
+# factory) is held to a strict baseline so downstream users get a fully-typed
+# API; the base implementations and providers (which wrap untyped cloud SDKs)
+# are temporarily exempted below and ratcheted to strict module-by-module.
+python_version = "3.13"
+files = ["cloudbridge"]
+ignore_missing_imports = true   # untyped cloud SDKs (boto3, azure-*, ...) -> Any
+namespace_packages = true
+warn_unused_configs = true
+# Strict baseline: applies to every module NOT exempted below.
+disallow_untyped_defs = true
+disallow_incomplete_defs = true
+no_implicit_optional = true
+check_untyped_defs = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+warn_return_any = true
+
+# Gradual-typing exemptions. Remove a module from this list once it is fully
+# annotated; it then falls under the strict baseline above.
+[[tool.mypy.overrides]]
+module = ["cloudbridge.base.*", "cloudbridge.providers.*"]
+ignore_errors = true

+ 9 - 1
tox.ini

@@ -6,7 +6,7 @@
 # running the tests.
 
 [tox]
-envlist = py3.13-{aws,azure,gcp,openstack,mock},lint
+envlist = py3.13-{aws,azure,gcp,openstack,mock},lint,mypy
 
 [testenv]
 commands = # see pyproject.toml for coverage options; setup.cfg for flake8
@@ -89,3 +89,11 @@ deps =
 [testenv:lint]
 commands = flake8 cloudbridge tests
 deps = flake8
+
+[testenv:mypy]
+# Type-check the public interface layer (see [tool.mypy] in pyproject.toml).
+# The interfaces import no third-party packages, so the package itself does not
+# need installing here, keeping this env fast.
+skip_install = true
+deps = mypy
+commands = mypy

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików