6
6
from enum import Enum
7
7
from inspect import getmodule
8
8
from threading import RLock
9
- from typing import Callable , List , Optional , Tuple
9
+ from typing import Any , Callable , Generic , List , Optional , ParamSpec , TypeVar
10
10
11
- from plux import Plugin , PluginManager , PluginSpec
11
+ from plux import Plugin , PluginManager , PluginSpec # type: ignore[import-untyped]
12
12
13
13
from localstack import config
14
14
@@ -24,7 +24,7 @@ class PackageException(Exception):
24
24
class NoSuchVersionException (PackageException ):
25
25
"""Exception indicating that a requested installer version is not available / supported."""
26
26
27
- def __init__ (self , package : str = None , version : str = None ):
27
+ def __init__ (self , package : str | None = None , version : str | None = None ):
28
28
message = "Unable to find requested version"
29
29
if package and version :
30
30
message += f"Unable to find requested version '{ version } ' for package '{ package } '"
@@ -123,6 +123,7 @@ def get_installed_dir(self) -> str | None:
123
123
directory = self ._get_install_dir (target )
124
124
if directory and os .path .exists (self ._get_install_marker_path (directory )):
125
125
return directory
126
+ return None
126
127
127
128
def _get_install_dir (self , target : InstallTarget ) -> str :
128
129
"""
@@ -181,7 +182,12 @@ def _post_process(self, target: InstallTarget) -> None:
181
182
pass
182
183
183
184
184
- class Package (abc .ABC ):
185
+ # With Python 3.13 we should be able to set PackageInstaller as the default
186
+ # https://typing.python.org/en/latest/spec/generics.html#type-parameter-defaults
187
+ T = TypeVar ("T" , bound = PackageInstaller )
188
+
189
+
190
+ class Package (abc .ABC , Generic [T ]):
185
191
"""
186
192
A Package defines a specific kind of software, mostly used as backends or supporting system for service
187
193
implementations.
@@ -214,7 +220,7 @@ def install(self, version: str | None = None, target: Optional[InstallTarget] =
214
220
self .get_installer (version ).install (target )
215
221
216
222
@functools .lru_cache ()
217
- def get_installer (self , version : str | None = None ) -> PackageInstaller :
223
+ def get_installer (self , version : str | None = None ) -> T :
218
224
"""
219
225
Returns the installer instance for a specific version of the package.
220
226
@@ -237,7 +243,7 @@ def get_versions(self) -> List[str]:
237
243
"""
238
244
raise NotImplementedError ()
239
245
240
- def _get_installer (self , version : str ) -> PackageInstaller :
246
+ def _get_installer (self , version : str ) -> T :
241
247
"""
242
248
Internal lookup function which needs to be implemented by specific packages.
243
249
It creates PackageInstaller instances for the specific version.
@@ -247,7 +253,7 @@ def _get_installer(self, version: str) -> PackageInstaller:
247
253
"""
248
254
raise NotImplementedError ()
249
255
250
- def __str__ (self ):
256
+ def __str__ (self ) -> str :
251
257
return self .name
252
258
253
259
@@ -298,7 +304,7 @@ def _get_install_marker_path(self, install_dir: str) -> str:
298
304
PLUGIN_NAMESPACE = "localstack.packages"
299
305
300
306
301
- class PackagesPlugin (Plugin ):
307
+ class PackagesPlugin (Plugin ): # type: ignore[misc]
302
308
"""
303
309
Plugin implementation for Package plugins.
304
310
A package plugin exposes a specific package instance.
@@ -311,8 +317,8 @@ def __init__(
311
317
self ,
312
318
name : str ,
313
319
scope : str ,
314
- get_package : Callable [[], Package | List [Package ]],
315
- should_load : Callable [[], bool ] = None ,
320
+ get_package : Callable [[], Package [ PackageInstaller ] | List [Package [ PackageInstaller ] ]],
321
+ should_load : Callable [[], bool ] | None = None ,
316
322
) -> None :
317
323
super ().__init__ ()
318
324
self .name = name
@@ -325,11 +331,11 @@ def should_load(self) -> bool:
325
331
return self ._should_load ()
326
332
return True
327
333
328
- def get_package (self ) -> Package :
334
+ def get_package (self ) -> Package [ PackageInstaller ] :
329
335
"""
330
336
:return: returns the package instance of this package plugin
331
337
"""
332
- return self ._get_package ()
338
+ return self ._get_package () # type: ignore[return-value]
333
339
334
340
335
341
class NoSuchPackageException (PackageException ):
@@ -338,28 +344,28 @@ class NoSuchPackageException(PackageException):
338
344
pass
339
345
340
346
341
- class PackagesPluginManager (PluginManager [PackagesPlugin ]):
347
+ class PackagesPluginManager (PluginManager [PackagesPlugin ]): # type: ignore[misc]
342
348
"""PluginManager which simplifies the loading / access of PackagesPlugins and their exposed package instances."""
343
349
344
- def __init__ (self ):
350
+ def __init__ (self ) -> None :
345
351
super ().__init__ (PLUGIN_NAMESPACE )
346
352
347
- def get_all_packages (self ) -> List [ Tuple [str , str , Package ]]:
353
+ def get_all_packages (self ) -> list [ tuple [str , str , Package [ PackageInstaller ] ]]:
348
354
return sorted (
349
355
[(plugin .name , plugin .scope , plugin .get_package ()) for plugin in self .load_all ()]
350
356
)
351
357
352
358
def get_packages (
353
- self , package_names : List [str ], version : Optional [str ] = None
354
- ) -> List [Package ]:
359
+ self , package_names : list [str ], version : Optional [str ] = None
360
+ ) -> list [Package [ PackageInstaller ] ]:
355
361
# Plugin names are unique, but there could be multiple packages with the same name in different scopes
356
362
plugin_specs_per_name = defaultdict (list )
357
363
# Plugin names have the format "<package-name>/<scope>", build a dict of specs per package name for the lookup
358
364
for plugin_spec in self .list_plugin_specs ():
359
365
(package_name , _ , _ ) = plugin_spec .name .rpartition ("/" )
360
366
plugin_specs_per_name [package_name ].append (plugin_spec )
361
367
362
- package_instances : List [Package ] = []
368
+ package_instances : list [Package [ PackageInstaller ] ] = []
363
369
for package_name in package_names :
364
370
plugin_specs = plugin_specs_per_name .get (package_name )
365
371
if not plugin_specs :
@@ -377,18 +383,24 @@ def get_packages(
377
383
return package_instances
378
384
379
385
386
+ P = ParamSpec ("P" )
387
+ T2 = TypeVar ("T2" )
388
+
389
+
380
390
def package (
381
- name : str = None , scope : str = "community" , should_load : Optional [Callable [[], bool ]] = None
382
- ):
391
+ name : str | None = None ,
392
+ scope : str = "community" ,
393
+ should_load : Optional [Callable [[], bool ]] = None ,
394
+ ) -> Callable [[Callable [[], Package [Any ] | list [Package [Any ]]]], PluginSpec ]:
383
395
"""
384
396
Decorator for marking methods that create Package instances as a PackagePlugin.
385
397
Methods marked with this decorator are discoverable as a PluginSpec within the namespace "localstack.packages",
386
398
with the name "<name>:<scope>". If api is not explicitly specified, then the parent module name is used as
387
399
service name.
388
400
"""
389
401
390
- def wrapper (fn ) :
391
- _name = name or getmodule (fn ).__name__ .split ("." )[- 2 ]
402
+ def wrapper (fn : Callable [[], Package [ Any ] | list [ Package [ Any ]]]) -> PluginSpec :
403
+ _name = name or getmodule (fn ).__name__ .split ("." )[- 2 ] # type: ignore[union-attr]
392
404
393
405
@functools .wraps (fn )
394
406
def factory () -> PackagesPlugin :
0 commit comments