Techdebt: MyPy Moto-API (#5663)
This commit is contained in:
parent
52892f5481
commit
9eccce8af3
@ -28,7 +28,7 @@ class ManagedState:
|
|||||||
self._tick += 1
|
self._tick += 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self) -> str:
|
||||||
"""
|
"""
|
||||||
Transitions the status as appropriate before returning
|
Transitions the status as appropriate before returning
|
||||||
"""
|
"""
|
||||||
@ -52,15 +52,15 @@ class ManagedState:
|
|||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@status.setter
|
@status.setter
|
||||||
def status(self, value):
|
def status(self, value: str) -> None:
|
||||||
self._status = value
|
self._status = value
|
||||||
|
|
||||||
def _get_next_status(self, previous):
|
def _get_next_status(self, previous: str) -> str:
|
||||||
return next(
|
return next(
|
||||||
(nxt for prev, nxt in self._transitions if previous == prev), previous
|
(nxt for prev, nxt in self._transitions if previous == prev), previous
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_last_status(self, previous):
|
def _get_last_status(self, previous: str) -> str:
|
||||||
next_state = self._get_next_status(previous)
|
next_state = self._get_next_status(previous)
|
||||||
while next_state != previous:
|
while next_state != previous:
|
||||||
previous = next_state
|
previous = next_state
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from moto.core import BaseBackend, DEFAULT_ACCOUNT_ID
|
from moto.core import BaseBackend, DEFAULT_ACCOUNT_ID
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
class MotoAPIBackend(BaseBackend):
|
class MotoAPIBackend(BaseBackend):
|
||||||
def reset(self):
|
def reset(self) -> None:
|
||||||
region_name = self.region_name
|
region_name = self.region_name
|
||||||
account_id = self.account_id
|
account_id = self.account_id
|
||||||
|
|
||||||
@ -13,19 +14,19 @@ class MotoAPIBackend(BaseBackend):
|
|||||||
continue
|
continue
|
||||||
for backend in backends_.values():
|
for backend in backends_.values():
|
||||||
backend.reset()
|
backend.reset()
|
||||||
self.__init__(region_name, account_id)
|
self.__init__(region_name, account_id) # type: ignore[misc]
|
||||||
|
|
||||||
def get_transition(self, model_name):
|
def get_transition(self, model_name: str) -> Dict[str, Any]:
|
||||||
from moto.moto_api import state_manager
|
from moto.moto_api import state_manager
|
||||||
|
|
||||||
return state_manager.get_transition(model_name)
|
return state_manager.get_transition(model_name)
|
||||||
|
|
||||||
def set_transition(self, model_name, transition):
|
def set_transition(self, model_name: str, transition: Dict[str, Any]) -> None:
|
||||||
from moto.moto_api import state_manager
|
from moto.moto_api import state_manager
|
||||||
|
|
||||||
state_manager.set_transition(model_name, transition)
|
state_manager.set_transition(model_name, transition)
|
||||||
|
|
||||||
def unset_transition(self, model_name):
|
def unset_transition(self, model_name: str) -> None:
|
||||||
from moto.moto_api import state_manager
|
from moto.moto_api import state_manager
|
||||||
|
|
||||||
state_manager.unset_transition(model_name)
|
state_manager.unset_transition(model_name)
|
||||||
|
@ -5,12 +5,12 @@ import os
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from botocore.awsrequest import AWSPreparedRequest
|
from botocore.awsrequest import AWSPreparedRequest
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, Union, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
class Recorder:
|
class Recorder:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self._location = str(os.environ.get("MOTO_RECORDER_FILEPATH", "moto_recording"))
|
self._location = str(os.environ.get("MOTO_RECORDER_FILEPATH", "moto_recording"))
|
||||||
self._os_enabled = bool(os.environ.get("MOTO_ENABLE_RECORDING", False))
|
self._os_enabled = bool(os.environ.get("MOTO_ENABLE_RECORDING", False))
|
||||||
self._user_enabled = self._os_enabled
|
self._user_enabled = self._os_enabled
|
||||||
@ -33,15 +33,15 @@ class Recorder:
|
|||||||
|
|
||||||
if body is None:
|
if body is None:
|
||||||
if isinstance(request, AWSPreparedRequest):
|
if isinstance(request, AWSPreparedRequest):
|
||||||
body, body_encoded = self._encode_body(body=request.body)
|
body_str, body_encoded = self._encode_body(body=request.body)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
request_body = None
|
request_body = None
|
||||||
request_body_size = int(request.headers["Content-Length"])
|
request_body_size = int(request.headers["Content-Length"])
|
||||||
request_body = request.environ["wsgi.input"].read(request_body_size)
|
request_body = request.environ["wsgi.input"].read(request_body_size)
|
||||||
body, body_encoded = self._encode_body(body=request_body)
|
body_str, body_encoded = self._encode_body(body=request_body)
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
body = ""
|
body_str = "" # type: ignore[]
|
||||||
body_encoded = False
|
body_encoded = False
|
||||||
finally:
|
finally:
|
||||||
if request_body is not None:
|
if request_body is not None:
|
||||||
@ -49,15 +49,15 @@ class Recorder:
|
|||||||
request_body = request_body.encode("utf-8")
|
request_body = request_body.encode("utf-8")
|
||||||
request.environ["wsgi.input"] = io.BytesIO(request_body)
|
request.environ["wsgi.input"] = io.BytesIO(request_body)
|
||||||
else:
|
else:
|
||||||
body, body_encoded = self._encode_body(body)
|
body_str, body_encoded = self._encode_body(body)
|
||||||
entry.update({"body": body, "body_encoded": body_encoded})
|
entry.update({"body": body_str, "body_encoded": body_encoded})
|
||||||
|
|
||||||
filepath = self._location
|
filepath = self._location
|
||||||
with open(filepath, "a+") as file:
|
with open(filepath, "a+") as file:
|
||||||
file.write(json.dumps(entry))
|
file.write(json.dumps(entry))
|
||||||
file.write("\n")
|
file.write("\n")
|
||||||
|
|
||||||
def _encode_body(self, body):
|
def _encode_body(self, body: Any) -> Tuple[str, bool]:
|
||||||
body_encoded = False
|
body_encoded = False
|
||||||
try:
|
try:
|
||||||
if isinstance(body, io.BytesIO):
|
if isinstance(body, io.BytesIO):
|
||||||
@ -69,7 +69,7 @@ class Recorder:
|
|||||||
body = None
|
body = None
|
||||||
return body, body_encoded
|
return body, body_encoded
|
||||||
|
|
||||||
def reset_recording(self):
|
def reset_recording(self) -> None:
|
||||||
"""
|
"""
|
||||||
Resets the recording. This will erase any requests made previously.
|
Resets the recording. This will erase any requests made previously.
|
||||||
"""
|
"""
|
||||||
@ -77,16 +77,16 @@ class Recorder:
|
|||||||
with open(filepath, "w"):
|
with open(filepath, "w"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start_recording(self):
|
def start_recording(self) -> None:
|
||||||
"""
|
"""
|
||||||
Start the recording, and append incoming requests to the log.
|
Start the recording, and append incoming requests to the log.
|
||||||
"""
|
"""
|
||||||
self._user_enabled = True
|
self._user_enabled = True
|
||||||
|
|
||||||
def stop_recording(self):
|
def stop_recording(self) -> None:
|
||||||
self._user_enabled = False
|
self._user_enabled = False
|
||||||
|
|
||||||
def upload_recording(self, data):
|
def upload_recording(self, data: Union[str, bytes]) -> None:
|
||||||
"""
|
"""
|
||||||
Replaces the current log. Remember to replay the recording afterwards.
|
Replaces the current log. Remember to replay the recording afterwards.
|
||||||
"""
|
"""
|
||||||
@ -96,7 +96,7 @@ class Recorder:
|
|||||||
with open(filepath, "bw") as file:
|
with open(filepath, "bw") as file:
|
||||||
file.write(data)
|
file.write(data)
|
||||||
|
|
||||||
def download_recording(self):
|
def download_recording(self) -> str:
|
||||||
"""
|
"""
|
||||||
Download the current recording. The result can be uploaded afterwards.
|
Download the current recording. The result can be uploaded afterwards.
|
||||||
"""
|
"""
|
||||||
@ -104,7 +104,7 @@ class Recorder:
|
|||||||
with open(filepath, "r") as file:
|
with open(filepath, "r") as file:
|
||||||
return file.read()
|
return file.read()
|
||||||
|
|
||||||
def replay_recording(self, target_host=None):
|
def replay_recording(self, target_host: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Replays the current log, i.e. replay all requests that were made after the recorder was started.
|
Replays the current log, i.e. replay all requests that were made after the recorder was started.
|
||||||
Download the recording if you want to manually verify the correct requests will be replayed.
|
Download the recording if you want to manually verify the correct requests will be replayed.
|
||||||
|
@ -1,31 +1,45 @@
|
|||||||
from ... import recorder
|
from ... import recorder
|
||||||
|
from moto.core.common_types import TYPE_RESPONSE
|
||||||
from moto.core.responses import BaseResponse
|
from moto.core.responses import BaseResponse
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class RecorderResponse(BaseResponse):
|
class RecorderResponse(BaseResponse):
|
||||||
def reset_recording(self, req, url, headers): # pylint: disable=unused-argument
|
def reset_recording(
|
||||||
|
self, req: Any, url: str, headers: Any # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
recorder.reset_recording()
|
recorder.reset_recording()
|
||||||
return 200, {}, ""
|
return 200, {}, ""
|
||||||
|
|
||||||
def start_recording(self, req, url, headers): # pylint: disable=unused-argument
|
def start_recording(
|
||||||
|
self, req: Any, url: str, headers: Any # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
recorder.start_recording()
|
recorder.start_recording()
|
||||||
return 200, {}, "Recording is set to True"
|
return 200, {}, "Recording is set to True"
|
||||||
|
|
||||||
def stop_recording(self, req, url, headers): # pylint: disable=unused-argument
|
def stop_recording(
|
||||||
|
self, req: Any, url: str, headers: Any # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
recorder.stop_recording()
|
recorder.stop_recording()
|
||||||
return 200, {}, "Recording is set to False"
|
return 200, {}, "Recording is set to False"
|
||||||
|
|
||||||
def upload_recording(self, req, url, headers): # pylint: disable=unused-argument
|
def upload_recording(
|
||||||
|
self, req: Any, url: str, headers: Any # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
data = req.data
|
data = req.data
|
||||||
recorder.upload_recording(data)
|
recorder.upload_recording(data)
|
||||||
return 200, {}, ""
|
return 200, {}, ""
|
||||||
|
|
||||||
def download_recording(self, req, url, headers): # pylint: disable=unused-argument
|
def download_recording(
|
||||||
|
self, req: Any, url: str, headers: Any # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
data = recorder.download_recording()
|
data = recorder.download_recording()
|
||||||
return 200, {}, data
|
return 200, {}, data
|
||||||
|
|
||||||
# NOTE: Replaying assumes, for simplicity, that it is the only action
|
# NOTE: Replaying assumes, for simplicity, that it is the only action
|
||||||
# running against moto at the time. No recording happens while replaying.
|
# running against moto at the time. No recording happens while replaying.
|
||||||
def replay_recording(self, req, url, headers): # pylint: disable=unused-argument
|
def replay_recording(
|
||||||
|
self, req: Any, url: str, headers: Any # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
recorder.replay_recording(target_host=url)
|
recorder.replay_recording(target_host=url)
|
||||||
return 200, {}, ""
|
return 200, {}, ""
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from moto import settings
|
from moto import settings
|
||||||
|
from moto.core.common_types import TYPE_RESPONSE
|
||||||
from moto.core.responses import ActionAuthenticatorMixin, BaseResponse
|
from moto.core.responses import ActionAuthenticatorMixin, BaseResponse
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
class MotoAPIResponse(BaseResponse):
|
class MotoAPIResponse(BaseResponse):
|
||||||
def reset_response(
|
def reset_response(
|
||||||
self, request, full_url, headers
|
self,
|
||||||
): # pylint: disable=unused-argument
|
request: Any, # pylint: disable=unused-argument
|
||||||
|
full_url: str, # pylint: disable=unused-argument
|
||||||
|
headers: Any, # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
from .models import moto_api_backend
|
from .models import moto_api_backend
|
||||||
|
|
||||||
@ -16,8 +21,11 @@ class MotoAPIResponse(BaseResponse):
|
|||||||
return 400, {}, json.dumps({"Error": "Need to POST to reset Moto"})
|
return 400, {}, json.dumps({"Error": "Need to POST to reset Moto"})
|
||||||
|
|
||||||
def reset_auth_response(
|
def reset_auth_response(
|
||||||
self, request, full_url, headers
|
self,
|
||||||
): # pylint: disable=unused-argument
|
request: Any, # pylint: disable=unused-argument
|
||||||
|
full_url: str, # pylint: disable=unused-argument
|
||||||
|
headers: Any, # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
previous_initial_no_auth_action_count = (
|
previous_initial_no_auth_action_count = (
|
||||||
settings.INITIAL_NO_AUTH_ACTION_COUNT
|
settings.INITIAL_NO_AUTH_ACTION_COUNT
|
||||||
@ -38,17 +46,22 @@ class MotoAPIResponse(BaseResponse):
|
|||||||
)
|
)
|
||||||
return 400, {}, json.dumps({"Error": "Need to POST to reset Moto Auth"})
|
return 400, {}, json.dumps({"Error": "Need to POST to reset Moto Auth"})
|
||||||
|
|
||||||
def model_data(self, request, full_url, headers): # pylint: disable=unused-argument
|
def model_data(
|
||||||
|
self,
|
||||||
|
request: Any, # pylint: disable=unused-argument
|
||||||
|
full_url: str, # pylint: disable=unused-argument
|
||||||
|
headers: Any, # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
from moto.core.base_backend import model_data
|
from moto.core.base_backend import model_data
|
||||||
|
|
||||||
results = {}
|
results: Dict[str, Dict[str, List[Any]]] = {}
|
||||||
for service in sorted(model_data):
|
for service in sorted(model_data):
|
||||||
models = model_data[service]
|
models = model_data[service]
|
||||||
results[service] = {}
|
results[service] = {}
|
||||||
for name in sorted(models):
|
for name in sorted(models):
|
||||||
model = models[name]
|
model = models[name]
|
||||||
results[service][name] = []
|
results[service][name] = []
|
||||||
for instance in model.instances:
|
for instance in model.instances: # type: ignore[attr-defined]
|
||||||
inst_result = {}
|
inst_result = {}
|
||||||
for attr in dir(instance):
|
for attr in dir(instance):
|
||||||
if not attr.startswith("_"):
|
if not attr.startswith("_"):
|
||||||
@ -61,14 +74,22 @@ class MotoAPIResponse(BaseResponse):
|
|||||||
results[service][name].append(inst_result)
|
results[service][name].append(inst_result)
|
||||||
return 200, {"Content-Type": "application/javascript"}, json.dumps(results)
|
return 200, {"Content-Type": "application/javascript"}, json.dumps(results)
|
||||||
|
|
||||||
def dashboard(self, request, full_url, headers): # pylint: disable=unused-argument
|
def dashboard(
|
||||||
|
self,
|
||||||
|
request: Any, # pylint: disable=unused-argument
|
||||||
|
full_url: str, # pylint: disable=unused-argument
|
||||||
|
headers: Any, # pylint: disable=unused-argument
|
||||||
|
) -> str:
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
return render_template("dashboard.html")
|
return render_template("dashboard.html")
|
||||||
|
|
||||||
def get_transition(
|
def get_transition(
|
||||||
self, request, full_url, headers
|
self,
|
||||||
): # pylint: disable=unused-argument
|
request: Any, # pylint: disable=unused-argument
|
||||||
|
full_url: str, # pylint: disable=unused-argument
|
||||||
|
headers: Any, # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
from .models import moto_api_backend
|
from .models import moto_api_backend
|
||||||
|
|
||||||
qs_dict = dict(
|
qs_dict = dict(
|
||||||
@ -81,8 +102,11 @@ class MotoAPIResponse(BaseResponse):
|
|||||||
return 200, {}, json.dumps(resp)
|
return 200, {}, json.dumps(resp)
|
||||||
|
|
||||||
def set_transition(
|
def set_transition(
|
||||||
self, request, full_url, headers
|
self,
|
||||||
): # pylint: disable=unused-argument
|
request: Any, # pylint: disable=unused-argument
|
||||||
|
full_url: str, # pylint: disable=unused-argument
|
||||||
|
headers: Any, # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
from .models import moto_api_backend
|
from .models import moto_api_backend
|
||||||
|
|
||||||
request_body_size = int(headers["Content-Length"])
|
request_body_size = int(headers["Content-Length"])
|
||||||
@ -95,8 +119,11 @@ class MotoAPIResponse(BaseResponse):
|
|||||||
return 201, {}, ""
|
return 201, {}, ""
|
||||||
|
|
||||||
def unset_transition(
|
def unset_transition(
|
||||||
self, request, full_url, headers
|
self,
|
||||||
): # pylint: disable=unused-argument
|
request: Any, # pylint: disable=unused-argument
|
||||||
|
full_url: str, # pylint: disable=unused-argument
|
||||||
|
headers: Any, # pylint: disable=unused-argument
|
||||||
|
) -> TYPE_RESPONSE:
|
||||||
from .models import moto_api_backend
|
from .models import moto_api_backend
|
||||||
|
|
||||||
request_body_size = int(headers["Content-Length"])
|
request_body_size = int(headers["Content-Length"])
|
||||||
@ -107,7 +134,7 @@ class MotoAPIResponse(BaseResponse):
|
|||||||
moto_api_backend.unset_transition(model_name)
|
moto_api_backend.unset_transition(model_name)
|
||||||
return 201, {}, ""
|
return 201, {}, ""
|
||||||
|
|
||||||
def seed(self, req, full_url, headers):
|
def seed(self, req: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:
|
||||||
self.setup_class(req, full_url, headers)
|
self.setup_class(req, full_url, headers)
|
||||||
from . import mock_random
|
from . import mock_random
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
from typing import Any, Dict
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TRANSITION = {"progression": "immediate"}
|
DEFAULT_TRANSITION = {"progression": "immediate"}
|
||||||
|
|
||||||
|
|
||||||
class StateManager:
|
class StateManager:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self._default_transitions = dict()
|
self._default_transitions: Dict[str, Dict[str, Any]] = dict()
|
||||||
self._transitions = dict()
|
self._transitions: Dict[str, Dict[str, Any]] = dict()
|
||||||
|
|
||||||
def register_default_transition(
|
def register_default_transition(
|
||||||
self, model_name: str, transition: Dict[str, Any]
|
self, model_name: str, transition: Dict[str, Any]
|
||||||
@ -18,7 +18,7 @@ class StateManager:
|
|||||||
"""
|
"""
|
||||||
self._default_transitions[model_name] = transition
|
self._default_transitions[model_name] = transition
|
||||||
|
|
||||||
def set_transition(self, model_name, transition):
|
def set_transition(self, model_name: str, transition: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Set a transition for a specific model. Any transition added here will take precedence over the default transition that was registered.
|
Set a transition for a specific model. Any transition added here will take precedence over the default transition that was registered.
|
||||||
|
|
||||||
@ -26,14 +26,14 @@ class StateManager:
|
|||||||
"""
|
"""
|
||||||
self._transitions[model_name] = transition
|
self._transitions[model_name] = transition
|
||||||
|
|
||||||
def unset_transition(self, model_name):
|
def unset_transition(self, model_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Unset (remove) a custom transition that was set. This is a safe and idempotent operation.
|
Unset (remove) a custom transition that was set. This is a safe and idempotent operation.
|
||||||
The default transition that was registered will not be altered by this operation.
|
The default transition that was registered will not be altered by this operation.
|
||||||
"""
|
"""
|
||||||
self._transitions.pop(model_name, None)
|
self._transitions.pop(model_name, None)
|
||||||
|
|
||||||
def get_transition(self, model_name):
|
def get_transition(self, model_name: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Return the configuration for a specific model. This will return a user-specified configuration, a default configuration of none exists, or the default transition if none exists.
|
Return the configuration for a specific model. This will return a user-specified configuration, a default configuration of none exists, or the default transition if none exists.
|
||||||
"""
|
"""
|
||||||
@ -43,5 +43,5 @@ class StateManager:
|
|||||||
return self._default_transitions[model_name]
|
return self._default_transitions[model_name]
|
||||||
return DEFAULT_TRANSITION
|
return DEFAULT_TRANSITION
|
||||||
|
|
||||||
def get_registered_models(self):
|
def get_registered_models(self) -> List[str]:
|
||||||
return list(self._default_transitions.keys())
|
return list(self._default_transitions.keys())
|
||||||
|
@ -18,7 +18,7 @@ disable = W,C,R,E
|
|||||||
enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import
|
enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import
|
||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
files= moto/a*,moto/b*,moto/c*
|
files= moto/a*,moto/b*,moto/c*,moto/moto_api
|
||||||
show_column_numbers=True
|
show_column_numbers=True
|
||||||
show_error_codes = True
|
show_error_codes = True
|
||||||
disable_error_code=abstract
|
disable_error_code=abstract
|
||||||
|
Loading…
Reference in New Issue
Block a user