Skip to content

Commit ce0a7e8

Browse files
committed
Core: Add type hints to aws/core.py
1 parent 9b6ef35 commit ce0a7e8

File tree

19 files changed

+110
-96
lines changed

19 files changed

+110
-96
lines changed

.circleci/config.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,16 @@ jobs:
164164
steps:
165165
- checkout
166166
- restore_cache:
167-
key: python-requirements-{{ checksum "requirements-dev.txt" }}
167+
key: python-requirements-{{ checksum "requirements-typehint.txt" }}
168168
- run:
169169
name: Setup environment
170170
command: |
171+
make install-dev-types
171172
make install
172173
mkdir -p target/reports
173174
mkdir -p target/coverage
174175
- save_cache:
175-
key: python-requirements-{{ checksum "requirements-dev.txt" }}
176+
key: python-requirements-{{ checksum "requirements-typehint.txt" }}
176177
paths:
177178
- "~/.cache/pip"
178179
- persist_to_workspace:

.github/actions/load-localstack-docker-from-artifacts/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ runs:
1818
with:
1919
python-version-file: '.python-version'
2020
cache: 'pip'
21-
cache-dependency-path: 'requirements-dev.txt'
21+
cache-dependency-path: 'requirements-typehint.txt'
2222

2323
- name: Install docker helper dependencies
2424
shell: bash

.github/actions/setup-tests-env/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ runs:
88
with:
99
python-version-file: '.python-version'
1010
cache: 'pip'
11-
cache-dependency-path: 'requirements-dev.txt'
11+
cache-dependency-path: 'requirements-typehint.txt'
1212

1313
- name: Install Community Dependencies
1414
shell: bash
15-
run: make install-dev
15+
run: make install-dev-types
1616

1717
- name: Setup environment
1818
shell: bash

.github/workflows/aws-main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686
fetch-depth: 0
8787

8888
- name: Load Localstack ${{ env.PLATFORM_NAME_AMD64 }} Docker Image
89-
uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master
89+
uses: ./.github/actions/load-localstack-docker-from-artifacts
9090
with:
9191
platform: ${{ env.PLATFORM_NAME_AMD64 }}
9292

@@ -115,7 +115,7 @@ jobs:
115115
TARGET_IMAGE_NAME="public.ecr.aws/localstack/localstack" ./bin/docker-helper.sh push
116116
117117
- name: Load Localstack ${{ env.PLATFORM_NAME_ARM64 }} Docker Image
118-
uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master
118+
uses: ./.github/actions/load-localstack-docker-from-artifacts
119119
with:
120120
platform: ${{ env.PLATFORM_NAME_ARM64 }}
121121

.github/workflows/aws-tests.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ jobs:
135135
fetch-depth: 0
136136

137137
- name: Build Image
138-
uses: localstack/localstack/.github/actions/build-image@master
138+
uses: ./.github/actions/build-image
139139
with:
140140
disableCaching: ${{ inputs.disableCaching == true && 'true' || 'false' }}
141141
dockerhubPullUsername: ${{ secrets.DOCKERHUB_PULL_USERNAME }}
@@ -179,7 +179,7 @@ jobs:
179179
fetch-depth: 0
180180

181181
- name: Prepare Local Test Environment
182-
uses: localstack/localstack/.github/actions/setup-tests-env@master
182+
uses: ./.github/actions/setup-tests-env
183183

184184
- name: Linting
185185
run: make lint
@@ -290,7 +290,7 @@ jobs:
290290
tests/aws/services/lambda_/functions/common
291291
292292
- name: Load Localstack Docker Image
293-
uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master
293+
uses: ./.github/actions/load-localstack-docker-from-artifacts
294294
with:
295295
platform: "${{ env.PLATFORM }}"
296296

@@ -356,10 +356,10 @@ jobs:
356356
fetch-depth: 0
357357

358358
- name: Prepare Local Test Environment
359-
uses: localstack/localstack/.github/actions/setup-tests-env@master
359+
uses: ./.github/actions/setup-tests-env
360360

361361
- name: Load Localstack Docker Image
362-
uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master
362+
uses: ./.github/actions/load-localstack-docker-from-artifacts
363363
with:
364364
platform: "${{ env.PLATFORM }}"
365365

@@ -430,7 +430,7 @@ jobs:
430430
fetch-depth: 0
431431

432432
- name: Load Localstack Docker Image
433-
uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master
433+
uses: ./.github/actions/load-localstack-docker-from-artifact
434434
with:
435435
platform: "${{ env.PLATFORM }}"
436436

@@ -477,7 +477,7 @@ jobs:
477477
uses: actions/checkout@v4
478478

479479
- name: Prepare Local Test Environment
480-
uses: localstack/localstack/.github/actions/setup-tests-env@master
480+
uses: ./.github/actions/setup-tests-env
481481

482482
- name: Run Cloudwatch v1 Provider Tests
483483
timeout-minutes: 30
@@ -520,7 +520,7 @@ jobs:
520520
uses: actions/checkout@v4
521521

522522
- name: Prepare Local Test Environment
523-
uses: localstack/localstack/.github/actions/setup-tests-env@master
523+
uses: ./.github/actions/setup-tests-env
524524

525525
- name: Download Test Selection
526526
if: ${{ env.TESTSELECTION_PYTEST_ARGS }}
@@ -569,7 +569,7 @@ jobs:
569569
uses: actions/checkout@v4
570570

571571
- name: Prepare Local Test Environment
572-
uses: localstack/localstack/.github/actions/setup-tests-env@master
572+
uses: ./.github/actions/setup-tests-env
573573

574574
- name: Download Test Selection
575575
if: ${{ env.TESTSELECTION_PYTEST_ARGS }}
@@ -620,7 +620,7 @@ jobs:
620620
uses: actions/checkout@v4
621621

622622
- name: Prepare Local Test Environment
623-
uses: localstack/localstack/.github/actions/setup-tests-env@master
623+
uses: ./.github/actions/setup-tests-env
624624

625625
- name: Download Test Selection
626626
if: ${{ env.TESTSELECTION_PYTEST_ARGS }}
@@ -672,7 +672,7 @@ jobs:
672672
fetch-depth: 0
673673

674674
- name: Load Localstack Docker Image
675-
uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master
675+
uses: ./.github/actions/load-localstack-docker-from-artifacts
676676
with:
677677
platform: "${{ env.PLATFORM }}"
678678

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ repos:
1515
hooks:
1616
- id: mypy
1717
entry: bash -c 'cd localstack-core && mypy --install-types --non-interactive'
18+
additional_dependencies: ['botocore-stubs', 'rolo']
1819

1920
- repo: https://github.com/pre-commit/pre-commit-hooks
2021
rev: v5.0.0

localstack-core/localstack/aws/api/core.py

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import functools
2-
from typing import Any, NamedTuple, Optional, Protocol, Type, TypedDict, Union
2+
from typing import (
3+
Any,
4+
Callable,
5+
NamedTuple,
6+
ParamSpec,
7+
Protocol,
8+
Type,
9+
TypedDict,
10+
TypeVar,
11+
)
312

413
from botocore.model import OperationModel, ServiceModel
514
from rolo.gateway import RequestContext as RoloRequestContext
@@ -13,6 +22,10 @@ class ServiceRequest(TypedDict):
1322
pass
1423

1524

25+
P = ParamSpec("P")
26+
T = TypeVar("T")
27+
28+
1629
ServiceResponse = Any
1730

1831

@@ -28,7 +41,7 @@ class ServiceException(Exception):
2841
sender_fault: bool
2942
message: str
3043

31-
def __init__(self, *args, **kwargs):
44+
def __init__(self, *args: Any, **kwargs: Any):
3245
super(ServiceException, self).__init__(*args)
3346

3447
if len(args) >= 1:
@@ -72,38 +85,38 @@ class RequestContext(RoloRequestContext):
7285
context, so it can be used for logging or modification before going to the serializer.
7386
"""
7487

75-
request: Optional[Request]
88+
request: Request
7689
"""The underlying incoming HTTP request."""
77-
service: Optional[ServiceModel]
90+
service: ServiceModel | None
7891
"""The botocore ServiceModel of the service the request is made to."""
79-
operation: Optional[OperationModel]
92+
operation: OperationModel | None
8093
"""The botocore OperationModel of the AWS operation being invoked."""
81-
region: Optional[str]
94+
region: str
8295
"""The region the request is made to."""
8396
partition: str
8497
"""The partition the request is made to."""
85-
account_id: Optional[str]
98+
account_id: str
8699
"""The account the request is made from."""
87-
request_id: Optional[str]
100+
request_id: str | None
88101
"""The autogenerated AWS request ID identifying the original request"""
89-
service_request: Optional[ServiceRequest]
102+
service_request: ServiceRequest | None
90103
"""The AWS operation parameters."""
91-
service_response: Optional[ServiceResponse]
104+
service_response: ServiceResponse | None
92105
"""The response from the AWS emulator backend."""
93-
service_exception: Optional[ServiceException]
106+
service_exception: ServiceException | None
94107
"""The exception the AWS emulator backend may have raised."""
95-
internal_request_params: Optional[InternalRequestParameters]
108+
internal_request_params: InternalRequestParameters | None
96109
"""Data sent by client-side LocalStack during internal calls."""
97-
trace_context: dict
110+
trace_context: dict[str, Any]
98111
"""Tracing metadata such as X-Ray trace headers"""
99112

100-
def __init__(self, request=None) -> None:
113+
def __init__(self, request: Request):
101114
super().__init__(request)
102115
self.service = None
103116
self.operation = None
104-
self.region = None
117+
self.region = None # type: ignore[assignment] # type=str, because we know it will always be set downstream
105118
self.partition = "aws" # Sensible default - will be overwritten by region-handler
106-
self.account_id = None
119+
self.account_id = None # type: ignore[assignment] # type=str, because we know it will always be set downstream
107120
self.request_id = long_uid()
108121
self.service_request = None
109122
self.service_response = None
@@ -119,7 +132,7 @@ def is_internal_call(self) -> bool:
119132
return self.internal_request_params is not None
120133

121134
@property
122-
def service_operation(self) -> Optional[ServiceOperation]:
135+
def service_operation(self) -> ServiceOperation | None:
123136
"""
124137
If both the service model and the operation model are set, this returns a tuple of the service name and
125138
operation name.
@@ -130,7 +143,7 @@ def service_operation(self) -> Optional[ServiceOperation]:
130143
return None
131144
return ServiceOperation(self.service.service_name, self.operation.name)
132145

133-
def __repr__(self):
146+
def __repr__(self) -> str:
134147
return f"<RequestContext {self.service=}, {self.operation=}, {self.region=}, {self.account_id=}, {self.request=}>"
135148

136149

@@ -141,7 +154,7 @@ class ServiceRequestHandler(Protocol):
141154

142155
def __call__(
143156
self, context: RequestContext, request: ServiceRequest
144-
) -> Optional[Union[ServiceResponse, Response]]:
157+
) -> ServiceResponse | Response | None:
145158
"""
146159
Handle the given request.
147160
@@ -152,19 +165,21 @@ def __call__(
152165
raise NotImplementedError
153166

154167

155-
def handler(operation: str = None, context: bool = True, expand: bool = True):
168+
def handler(
169+
operation: str | None = None, context: bool = True, expand: bool = True
170+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
156171
"""
157172
Decorator that indicates that the given function is a handler
158173
"""
159174

160-
def wrapper(fn):
175+
def wrapper(fn: Callable[P, T]) -> Callable[P, T]:
161176
@functools.wraps(fn)
162-
def operation_marker(*args, **kwargs):
177+
def operation_marker(*args: P.args, **kwargs: P.kwargs) -> T:
163178
return fn(*args, **kwargs)
164179

165-
operation_marker.operation = operation
166-
operation_marker.expand_parameters = expand
167-
operation_marker.pass_context = context
180+
operation_marker.operation = operation # type: ignore[attr-defined]
181+
operation_marker.expand_parameters = expand # type: ignore[attr-defined]
182+
operation_marker.pass_context = context # type: ignore[attr-defined]
168183

169184
return operation_marker
170185

localstack-core/localstack/aws/client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,7 @@ def __call__(
395395
operation: OperationModel = request.operation_model
396396

397397
# create request
398-
context = RequestContext()
399-
context.request = create_http_request(request)
398+
context = RequestContext(request=create_http_request(request))
400399

401400
# TODO: just a hacky thing to unblock the service model being set to `sqs-query` blocking for now
402401
# this is using the same services as `localstack.aws.protocol.service_router.resolve_conflicts`, maybe

localstack-core/localstack/aws/forwarder.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,10 @@ def create_aws_request_context(
262262
)
263263

264264
aws_request: AWSPreparedRequest = client._endpoint.create_request(request_dict, operation)
265-
context = RequestContext()
265+
context = RequestContext(request=create_http_request(aws_request))
266266
context.service = service
267267
context.operation = operation
268268
context.region = region
269-
context.request = create_http_request(aws_request)
270269
context.service_request = parameters
271270

272271
return context

localstack-core/localstack/testing/aws/util.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,14 @@ def create_client_with_keys(
108108
def create_request_context(
109109
service_name: str, operation_name: str, region: str, aws_request: AWSPreparedRequest
110110
) -> RequestContext:
111-
context = RequestContext()
111+
if hasattr(aws_request.body, "read"):
112+
aws_request.body = aws_request.body.read()
113+
request = create_http_request(aws_request)
114+
115+
context = RequestContext(request=request)
112116
context.service = load_service(service_name)
113117
context.operation = context.service.operation_model(operation_name=operation_name)
114118
context.region = region
115-
if hasattr(aws_request.body, "read"):
116-
aws_request.body = aws_request.body.read()
117-
context.request = create_http_request(aws_request)
118119
parser = create_parser(context.service)
119120
_, instance = parser.parse(context.request)
120121
context.service_request = instance

localstack-core/mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[mypy]
22
explicit_package_bases = true
33
mypy_path=localstack-core
4-
files=localstack/packages,localstack/services/kinesis/packages.py
4+
files=localstack/aws/api/core.py,localstack/packages,localstack/services/kinesis/packages.py
55
ignore_missing_imports = False
66
follow_imports = silent
77
ignore_errors = False

tests/aws/test_moto.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,10 @@ def test_call_with_sns_with_full_uri():
246246
headers={"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"},
247247
)
248248
sns_service = load_service("sns")
249-
context = RequestContext()
249+
context = RequestContext(sns_request)
250250
context.account = "test"
251251
context.region = "us-west-1"
252252
context.service = sns_service
253-
context.request = sns_request
254253
context.operation = sns_service.operation_model("CreateTopic")
255254

256255
create_topic_response = moto.call_moto(context)

tests/integration/test_forwarder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_request_forwarder(_, __) -> ServiceResponse:
2525

2626
# invoke the function and expect the result from the fallback function
2727
dispatcher = ForwardingFallbackDispatcher(test_provider, test_request_forwarder)
28-
assert dispatcher["TestOperation"](RequestContext(), ServiceRequest()) == "fallback-result"
28+
assert dispatcher["TestOperation"](RequestContext(None), ServiceRequest()) == "fallback-result"
2929

3030

3131
def test_forwarding_fallback_dispatcher_avoid_fallback():
@@ -44,4 +44,4 @@ def test_request_forwarder(_, __) -> ServiceResponse:
4444
# expect a NotImplementedError exception (and not the ServiceException from the fallthrough)
4545
dispatcher = ForwardingFallbackDispatcher(test_provider, test_request_forwarder)
4646
with pytest.raises(NotImplementedError):
47-
dispatcher["TestOperation"](RequestContext(), ServiceRequest())
47+
dispatcher["TestOperation"](RequestContext(None), ServiceRequest())

tests/unit/aws/handlers/analytics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_ignores_requests_without_service(self):
4040
counter = ServiceRequestCounter(service_request_aggregator=aggregator)
4141

4242
chain = HandlerChain([counter])
43-
chain.handle(RequestContext(), Response())
43+
chain.handle(RequestContext(None), Response())
4444

4545
aggregator.start.assert_not_called()
4646
aggregator.add_request.assert_not_called()

0 commit comments

Comments
 (0)