Skip to content

Commit 96a0216

Browse files
authored
Admin: Enable mypy during linting (#12532)
1 parent 4d7a031 commit 96a0216

19 files changed

+134
-188
lines changed

.pre-commit-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ repos:
1010
# Run the formatter.
1111
- id: ruff-format
1212

13+
- repo: https://github.com/pre-commit/mirrors-mypy
14+
rev: v1.15.0
15+
hooks:
16+
- id: mypy
17+
entry: bash -c 'cd localstack-core && mypy --install-types --non-interactive'
18+
1319
- repo: https://github.com/pre-commit/pre-commit-hooks
1420
rev: v5.0.0
1521
hooks:

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ lint: ## Run code linter to check code style, check if formatte
121121
($(VENV_RUN); python -m ruff check --output-format=full . && python -m ruff format --check --diff .)
122122
$(VENV_RUN); pre-commit run check-pinned-deps-for-needed-upgrade --files pyproject.toml # run pre-commit hook manually here to ensure that this check runs in CI as well
123123
$(VENV_RUN); openapi-spec-validator localstack-core/localstack/openapi.yaml
124+
$(VENV_RUN); cd localstack-core && mypy --install-types --non-interactive
124125

125126
lint-modified: ## Run code linter to check code style, check if formatter would make changes on modified files, and check if dependency pins need to be updated because of modified files
126127
($(VENV_RUN); python -m ruff check --output-format=full `git diff --diff-filter=d --name-only HEAD | grep '\.py$$' | xargs` && python -m ruff format --check `git diff --diff-filter=d --name-only HEAD | grep '\.py$$' | xargs`)

localstack-core/localstack/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ def _get_unprivileged_port_range_start(self) -> int:
605605
def is_unprivileged(self) -> bool:
606606
return self.port >= self._get_unprivileged_port_range_start()
607607

608-
def host_and_port(self):
608+
def host_and_port(self) -> str:
609609
formatted_host = f"[{self.host}]" if is_ipv6_address(self.host) else self.host
610610
return f"{formatted_host}:{self.port}" if self.port is not None else formatted_host
611611

localstack-core/localstack/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
LOCALSTACK_ROOT_FOLDER = os.path.realpath(os.path.join(MODULE_MAIN_PATH, ".."))
4646

4747
# virtualenv folder
48-
LOCALSTACK_VENV_FOLDER = os.environ.get("VIRTUAL_ENV")
48+
LOCALSTACK_VENV_FOLDER: str = os.environ.get("VIRTUAL_ENV")
4949
if not LOCALSTACK_VENV_FOLDER:
5050
# fallback to the previous logic
5151
LOCALSTACK_VENV_FOLDER = os.path.join(LOCALSTACK_ROOT_FOLDER, ".venv")

localstack-core/localstack/packages/api.py

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
from enum import Enum
77
from inspect import getmodule
88
from threading import RLock
9-
from typing import Callable, List, Optional, Tuple
9+
from typing import Any, Callable, Generic, List, Optional, ParamSpec, TypeVar
1010

11-
from plux import Plugin, PluginManager, PluginSpec
11+
from plux import Plugin, PluginManager, PluginSpec # type: ignore[import-untyped]
1212

1313
from localstack import config
1414

@@ -24,7 +24,7 @@ class PackageException(Exception):
2424
class NoSuchVersionException(PackageException):
2525
"""Exception indicating that a requested installer version is not available / supported."""
2626

27-
def __init__(self, package: str = None, version: str = None):
27+
def __init__(self, package: str | None = None, version: str | None = None):
2828
message = "Unable to find requested version"
2929
if package and version:
3030
message += f"Unable to find requested version '{version}' for package '{package}'"
@@ -123,6 +123,7 @@ def get_installed_dir(self) -> str | None:
123123
directory = self._get_install_dir(target)
124124
if directory and os.path.exists(self._get_install_marker_path(directory)):
125125
return directory
126+
return None
126127

127128
def _get_install_dir(self, target: InstallTarget) -> str:
128129
"""
@@ -181,7 +182,12 @@ def _post_process(self, target: InstallTarget) -> None:
181182
pass
182183

183184

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]):
185191
"""
186192
A Package defines a specific kind of software, mostly used as backends or supporting system for service
187193
implementations.
@@ -214,7 +220,7 @@ def install(self, version: str | None = None, target: Optional[InstallTarget] =
214220
self.get_installer(version).install(target)
215221

216222
@functools.lru_cache()
217-
def get_installer(self, version: str | None = None) -> PackageInstaller:
223+
def get_installer(self, version: str | None = None) -> T:
218224
"""
219225
Returns the installer instance for a specific version of the package.
220226
@@ -237,7 +243,7 @@ def get_versions(self) -> List[str]:
237243
"""
238244
raise NotImplementedError()
239245

240-
def _get_installer(self, version: str) -> PackageInstaller:
246+
def _get_installer(self, version: str) -> T:
241247
"""
242248
Internal lookup function which needs to be implemented by specific packages.
243249
It creates PackageInstaller instances for the specific version.
@@ -247,7 +253,7 @@ def _get_installer(self, version: str) -> PackageInstaller:
247253
"""
248254
raise NotImplementedError()
249255

250-
def __str__(self):
256+
def __str__(self) -> str:
251257
return self.name
252258

253259

@@ -298,7 +304,7 @@ def _get_install_marker_path(self, install_dir: str) -> str:
298304
PLUGIN_NAMESPACE = "localstack.packages"
299305

300306

301-
class PackagesPlugin(Plugin):
307+
class PackagesPlugin(Plugin): # type: ignore[misc]
302308
"""
303309
Plugin implementation for Package plugins.
304310
A package plugin exposes a specific package instance.
@@ -311,8 +317,8 @@ def __init__(
311317
self,
312318
name: str,
313319
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,
316322
) -> None:
317323
super().__init__()
318324
self.name = name
@@ -325,11 +331,11 @@ def should_load(self) -> bool:
325331
return self._should_load()
326332
return True
327333

328-
def get_package(self) -> Package:
334+
def get_package(self) -> Package[PackageInstaller]:
329335
"""
330336
:return: returns the package instance of this package plugin
331337
"""
332-
return self._get_package()
338+
return self._get_package() # type: ignore[return-value]
333339

334340

335341
class NoSuchPackageException(PackageException):
@@ -338,28 +344,28 @@ class NoSuchPackageException(PackageException):
338344
pass
339345

340346

341-
class PackagesPluginManager(PluginManager[PackagesPlugin]):
347+
class PackagesPluginManager(PluginManager[PackagesPlugin]): # type: ignore[misc]
342348
"""PluginManager which simplifies the loading / access of PackagesPlugins and their exposed package instances."""
343349

344-
def __init__(self):
350+
def __init__(self) -> None:
345351
super().__init__(PLUGIN_NAMESPACE)
346352

347-
def get_all_packages(self) -> List[Tuple[str, str, Package]]:
353+
def get_all_packages(self) -> list[tuple[str, str, Package[PackageInstaller]]]:
348354
return sorted(
349355
[(plugin.name, plugin.scope, plugin.get_package()) for plugin in self.load_all()]
350356
)
351357

352358
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]]:
355361
# Plugin names are unique, but there could be multiple packages with the same name in different scopes
356362
plugin_specs_per_name = defaultdict(list)
357363
# Plugin names have the format "<package-name>/<scope>", build a dict of specs per package name for the lookup
358364
for plugin_spec in self.list_plugin_specs():
359365
(package_name, _, _) = plugin_spec.name.rpartition("/")
360366
plugin_specs_per_name[package_name].append(plugin_spec)
361367

362-
package_instances: List[Package] = []
368+
package_instances: list[Package[PackageInstaller]] = []
363369
for package_name in package_names:
364370
plugin_specs = plugin_specs_per_name.get(package_name)
365371
if not plugin_specs:
@@ -377,18 +383,24 @@ def get_packages(
377383
return package_instances
378384

379385

386+
P = ParamSpec("P")
387+
T2 = TypeVar("T2")
388+
389+
380390
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]:
383395
"""
384396
Decorator for marking methods that create Package instances as a PackagePlugin.
385397
Methods marked with this decorator are discoverable as a PluginSpec within the namespace "localstack.packages",
386398
with the name "<name>:<scope>". If api is not explicitly specified, then the parent module name is used as
387399
service name.
388400
"""
389401

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]
392404

393405
@functools.wraps(fn)
394406
def factory() -> PackagesPlugin:

localstack-core/localstack/packages/core.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from abc import ABC
55
from functools import lru_cache
66
from sys import version_info
7-
from typing import Optional, Tuple
7+
from typing import Any, Optional, Tuple
88

99
import requests
1010

@@ -39,6 +39,7 @@ def get_executable_path(self) -> str | None:
3939
install_dir = self.get_installed_dir()
4040
if install_dir:
4141
return self._get_install_marker_path(install_dir)
42+
return None
4243

4344

4445
class DownloadInstaller(ExecutableInstaller):
@@ -104,6 +105,7 @@ def get_executable_path(self) -> str | None:
104105
if install_dir:
105106
install_dir = install_dir[: -len(subdir)]
106107
return self._get_install_marker_path(install_dir)
108+
return None
107109

108110
def _install(self, target: InstallTarget) -> None:
109111
target_directory = self._get_install_dir(target)
@@ -133,7 +135,7 @@ def _install(self, target: InstallTarget) -> None:
133135
class PermissionDownloadInstaller(DownloadInstaller, ABC):
134136
def _install(self, target: InstallTarget) -> None:
135137
super()._install(target)
136-
chmod_r(self.get_executable_path(), 0o777)
138+
chmod_r(self.get_executable_path(), 0o777) # type: ignore[arg-type]
137139

138140

139141
class GitHubReleaseInstaller(PermissionDownloadInstaller):
@@ -249,11 +251,11 @@ class PythonPackageInstaller(PackageInstaller):
249251
normalized_name: str
250252
"""Normalized package name according to PEP440."""
251253

252-
def __init__(self, name: str, version: str, *args, **kwargs):
254+
def __init__(self, name: str, version: str, *args: Any, **kwargs: Any):
253255
super().__init__(name, version, *args, **kwargs)
254256
self.normalized_name = self._normalize_package_name(name)
255257

256-
def _normalize_package_name(self, name: str):
258+
def _normalize_package_name(self, name: str) -> str:
257259
"""
258260
Normalized the Python package name according to PEP440.
259261
https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization

localstack-core/localstack/packages/debugpy.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
from localstack.utils.run import run
55

66

7-
class DebugPyPackage(Package):
8-
def __init__(self):
7+
class DebugPyPackage(Package["DebugPyPackageInstaller"]):
8+
def __init__(self) -> None:
99
super().__init__("DebugPy", "latest")
1010

1111
def get_versions(self) -> List[str]:
1212
return ["latest"]
1313

14-
def _get_installer(self, version: str) -> PackageInstaller:
14+
def _get_installer(self, version: str) -> "DebugPyPackageInstaller":
1515
return DebugPyPackageInstaller("debugpy", version)
1616

1717

@@ -20,7 +20,7 @@ class DebugPyPackageInstaller(PackageInstaller):
2020

2121
def is_installed(self) -> bool:
2222
try:
23-
import debugpy # noqa: T100
23+
import debugpy # type: ignore[import-not-found] # noqa: T100
2424

2525
assert debugpy
2626
return True

localstack-core/localstack/packages/ffmpeg.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
from typing import List
33

4-
from localstack.packages import Package, PackageInstaller
4+
from localstack.packages import Package
55
from localstack.packages.core import ArchiveDownloadAndExtractInstaller
66
from localstack.utils.platform import get_arch
77

@@ -10,11 +10,11 @@
1010
)
1111

1212

13-
class FfmpegPackage(Package):
14-
def __init__(self):
13+
class FfmpegPackage(Package["FfmpegPackageInstaller"]):
14+
def __init__(self) -> None:
1515
super().__init__(name="ffmpeg", default_version="7.0.1")
1616

17-
def _get_installer(self, version: str) -> PackageInstaller:
17+
def _get_installer(self, version: str) -> "FfmpegPackageInstaller":
1818
return FfmpegPackageInstaller(version)
1919

2020
def get_versions(self) -> List[str]:
@@ -35,10 +35,10 @@ def _get_archive_subdir(self) -> str:
3535
return f"ffmpeg-{self.version}-{get_arch()}-static"
3636

3737
def get_ffmpeg_path(self) -> str:
38-
return os.path.join(self.get_installed_dir(), "ffmpeg")
38+
return os.path.join(self.get_installed_dir(), "ffmpeg") # type: ignore[arg-type]
3939

4040
def get_ffprobe_path(self) -> str:
41-
return os.path.join(self.get_installed_dir(), "ffprobe")
41+
return os.path.join(self.get_installed_dir(), "ffprobe") # type: ignore[arg-type]
4242

4343

4444
ffmpeg_package = FfmpegPackage()

localstack-core/localstack/packages/java.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ def get_java_lib_path(self) -> str | None:
4747
if is_mac_os():
4848
return os.path.join(java_home, "lib", "jli", "libjli.dylib")
4949
return os.path.join(java_home, "lib", "server", "libjvm.so")
50+
return None
5051

51-
def get_java_env_vars(self, path: str = None, ld_library_path: str = None) -> dict[str, str]:
52+
def get_java_env_vars(
53+
self, path: str | None = None, ld_library_path: str | None = None
54+
) -> dict[str, str]:
5255
"""
5356
Returns environment variables pointing to the Java installation. This is useful to build the environment where
5457
the application will run.
@@ -64,16 +67,16 @@ def get_java_env_vars(self, path: str = None, ld_library_path: str = None) -> di
6467

6568
path = path or os.environ["PATH"]
6669

67-
ld_library_path = ld_library_path or os.environ.get("LD_LIBRARY_PATH")
70+
library_path = ld_library_path or os.environ.get("LD_LIBRARY_PATH")
6871
# null paths (e.g. `:/foo`) have a special meaning according to the manpages
69-
if ld_library_path is None:
70-
ld_library_path = f"{java_home}/lib:{java_home}/lib/server"
72+
if library_path is None:
73+
full_library_path = f"{java_home}/lib:{java_home}/lib/server"
7174
else:
72-
ld_library_path = f"{java_home}/lib:{java_home}/lib/server:{ld_library_path}"
75+
full_library_path = f"{java_home}/lib:{java_home}/lib/server:{library_path}"
7376

7477
return {
75-
"JAVA_HOME": java_home,
76-
"LD_LIBRARY_PATH": ld_library_path,
78+
"JAVA_HOME": java_home, # type: ignore[dict-item]
79+
"LD_LIBRARY_PATH": full_library_path,
7780
"PATH": f"{java_bin}:{path}",
7881
}
7982

@@ -144,7 +147,7 @@ def get_java_home(self) -> str | None:
144147
"""
145148
installed_dir = self.get_installed_dir()
146149
if is_mac_os():
147-
return os.path.join(installed_dir, "Contents", "Home")
150+
return os.path.join(installed_dir, "Contents", "Home") # type: ignore[arg-type]
148151
return installed_dir
149152

150153
@property
@@ -188,14 +191,14 @@ def _download_url_fallback(self) -> str:
188191
)
189192

190193

191-
class JavaPackage(Package):
194+
class JavaPackage(Package[JavaPackageInstaller]):
192195
def __init__(self, default_version: str = DEFAULT_JAVA_VERSION):
193196
super().__init__(name="Java", default_version=default_version)
194197

195198
def get_versions(self) -> List[str]:
196199
return list(JAVA_VERSIONS.keys())
197200

198-
def _get_installer(self, version):
201+
def _get_installer(self, version: str) -> JavaPackageInstaller:
199202
return JavaPackageInstaller(version)
200203

201204

0 commit comments

Comments
 (0)