Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0b750593da | ||
|
5b1686ceba | ||
|
86baaef1ec | ||
|
2b4b78da88 | ||
|
ee6d8cfe11 | ||
|
c4da66bf1f | ||
|
d50d3b8448 | ||
|
313f97ac77 | ||
|
a23547d73b | ||
|
34d1dee407 |
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -2,9 +2,13 @@ name: "CodeQL"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '25 16 * * 0'
|
- cron: '25 16 * * 0'
|
||||||
|
|
1
.github/workflows/coverage.yml
vendored
1
.github/workflows/coverage.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
tags:
|
tags:
|
||||||
- "!*" # Do not execute on tags
|
- "!*" # Do not execute on tags
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
1
.github/workflows/pr-bandit.yml
vendored
1
.github/workflows/pr-bandit.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
1
.github/workflows/pr-docs.yml
vendored
1
.github/workflows/pr-docs.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
1
.github/workflows/pr-linter.yml
vendored
1
.github/workflows/pr-linter.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
1
.github/workflows/pr-python310.yml
vendored
1
.github/workflows/pr-python310.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
1
.github/workflows/pr-python37.yml
vendored
1
.github/workflows/pr-python37.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
1
.github/workflows/pr-python38.yml
vendored
1
.github/workflows/pr-python38.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
1
.github/workflows/pr-python39.yml
vendored
1
.github/workflows/pr-python39.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
3
.github/workflows/pr-type-check.yml
vendored
3
.github/workflows/pr-type-check.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -15,7 +16,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
config:
|
config:
|
||||||
- { python-version: 3.7, tox-env: type-checking}
|
# - { python-version: 3.7, tox-env: type-checking}
|
||||||
- { python-version: 3.8, tox-env: type-checking}
|
- { python-version: 3.8, tox-env: type-checking}
|
||||||
- { python-version: 3.9, tox-env: type-checking}
|
- { python-version: 3.9, tox-env: type-checking}
|
||||||
- { python-version: "3.10", tox-env: type-checking}
|
- { python-version: "3.10", tox-env: type-checking}
|
||||||
|
|
1
.github/workflows/pr-windows.yml
vendored
1
.github/workflows/pr-windows.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "*LTS"
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
28
codecov.yml
Normal file
28
codecov.yml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 0.75
|
||||||
|
informational: true
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 0.5
|
||||||
|
precision: 3
|
||||||
|
codecov:
|
||||||
|
require_ci_to_pass: false
|
||||||
|
ignore:
|
||||||
|
- "sanic/__main__.py"
|
||||||
|
- "sanic/compat.py"
|
||||||
|
- "sanic/reloader_helpers.py"
|
||||||
|
- "sanic/simple.py"
|
||||||
|
- "sanic/utils.py"
|
||||||
|
- "sanic/cli"
|
||||||
|
- ".github/"
|
||||||
|
- "changelogs/"
|
||||||
|
- "docker/"
|
||||||
|
- "docs/"
|
||||||
|
- "examples/"
|
||||||
|
- "scripts/"
|
||||||
|
- "tests/"
|
|
@ -1,3 +1,9 @@
|
||||||
|
## Version 21.12.1
|
||||||
|
|
||||||
|
- [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup
|
||||||
|
- [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7
|
||||||
|
- [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values
|
||||||
|
|
||||||
## Version 21.12.0
|
## Version 21.12.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = "21.12.0"
|
__version__ = "21.12.2"
|
||||||
|
|
58
sanic/app.py
58
sanic/app.py
|
@ -1552,10 +1552,19 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
register: bool = True,
|
register: bool = True,
|
||||||
) -> Task:
|
) -> Task:
|
||||||
prepped = cls._prep_task(task, app, loop)
|
if not isinstance(task, Future):
|
||||||
task = loop.create_task(prepped, name=name)
|
prepped = cls._prep_task(task, app, loop)
|
||||||
|
if sys.version_info < (3, 8):
|
||||||
|
if name:
|
||||||
|
error_logger.warning(
|
||||||
|
"Cannot set a name for a task when using Python 3.7. "
|
||||||
|
"Your task will be created without a name."
|
||||||
|
)
|
||||||
|
task = loop.create_task(prepped)
|
||||||
|
else:
|
||||||
|
task = loop.create_task(prepped, name=name)
|
||||||
|
|
||||||
if name and register:
|
if name and register and sys.version_info > (3, 7):
|
||||||
app._task_registry[name] = task
|
app._task_registry[name] = task
|
||||||
|
|
||||||
return task
|
return task
|
||||||
|
@ -1617,10 +1626,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
def get_task(
|
def get_task(
|
||||||
self, name: str, *, raise_exception: bool = True
|
self, name: str, *, raise_exception: bool = True
|
||||||
) -> Optional[Task]:
|
) -> Optional[Task]:
|
||||||
if sys.version_info == (3, 7):
|
if sys.version_info < (3, 8):
|
||||||
raise RuntimeError(
|
error_logger.warning(
|
||||||
"This feature is only supported on using Python 3.8+."
|
"This feature (get_task) is only supported on using "
|
||||||
|
"Python 3.8+."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
return self._task_registry[name]
|
return self._task_registry[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -1637,10 +1648,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
*,
|
*,
|
||||||
raise_exception: bool = True,
|
raise_exception: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
if sys.version_info == (3, 7):
|
if sys.version_info < (3, 8):
|
||||||
raise RuntimeError(
|
error_logger.warning(
|
||||||
"This feature is only supported on using Python 3.8+."
|
"This feature (cancel_task) is only supported on using "
|
||||||
|
"Python 3.8+."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
task = self.get_task(name, raise_exception=raise_exception)
|
task = self.get_task(name, raise_exception=raise_exception)
|
||||||
if task and not task.cancelled():
|
if task and not task.cancelled():
|
||||||
args: Tuple[str, ...] = ()
|
args: Tuple[str, ...] = ()
|
||||||
|
@ -1659,10 +1672,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
...
|
...
|
||||||
|
|
||||||
def purge_tasks(self):
|
def purge_tasks(self):
|
||||||
if sys.version_info == (3, 7):
|
if sys.version_info < (3, 8):
|
||||||
raise RuntimeError(
|
error_logger.warning(
|
||||||
"This feature is only supported on using Python 3.8+."
|
"This feature (purge_tasks) is only supported on using "
|
||||||
|
"Python 3.8+."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
for task in self.tasks:
|
for task in self.tasks:
|
||||||
if task.done() or task.cancelled():
|
if task.done() or task.cancelled():
|
||||||
name = task.get_name()
|
name = task.get_name()
|
||||||
|
@ -1675,10 +1690,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
def shutdown_tasks(
|
def shutdown_tasks(
|
||||||
self, timeout: Optional[float] = None, increment: float = 0.1
|
self, timeout: Optional[float] = None, increment: float = 0.1
|
||||||
):
|
):
|
||||||
if sys.version_info == (3, 7):
|
if sys.version_info < (3, 8):
|
||||||
raise RuntimeError(
|
error_logger.warning(
|
||||||
"This feature is only supported on using Python 3.8+."
|
"This feature (shutdown_tasks) is only supported on using "
|
||||||
|
"Python 3.8+."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
for task in self.tasks:
|
for task in self.tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
|
@ -1692,10 +1709,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tasks(self):
|
def tasks(self):
|
||||||
if sys.version_info == (3, 7):
|
if sys.version_info < (3, 8):
|
||||||
raise RuntimeError(
|
error_logger.warning(
|
||||||
"This feature is only supported on using Python 3.8+."
|
"This feature (tasks) is only supported on using "
|
||||||
|
"Python 3.8+."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
return iter(self._task_registry.values())
|
return iter(self._task_registry.values())
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
@ -1709,7 +1728,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
details: https://asgi.readthedocs.io/en/latest
|
details: https://asgi.readthedocs.io/en/latest
|
||||||
"""
|
"""
|
||||||
self.asgi = True
|
self.asgi = True
|
||||||
self.motd("")
|
if scope["type"] == "lifespan":
|
||||||
|
self.motd("")
|
||||||
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
|
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||||
asgi_app = self._asgi_app
|
asgi_app = self._asgi_app
|
||||||
await asgi_app()
|
await asgi_app()
|
||||||
|
|
|
@ -124,22 +124,27 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||||
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
||||||
|
|
||||||
def __setattr__(self, attr, value) -> None:
|
def __setattr__(self, attr, value) -> None:
|
||||||
if attr in self.__class__.__setters__:
|
|
||||||
try:
|
|
||||||
super().__setattr__(attr, value)
|
|
||||||
except AttributeError:
|
|
||||||
...
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
self.update({attr: value})
|
self.update({attr: value})
|
||||||
|
|
||||||
def __setitem__(self, attr, value) -> None:
|
def __setitem__(self, attr, value) -> None:
|
||||||
self.update({attr: value})
|
self.update({attr: value})
|
||||||
|
|
||||||
def update(self, *other, **kwargs) -> None:
|
def update(self, *other, **kwargs) -> None:
|
||||||
other_mapping = {k: v for item in other for k, v in dict(item).items()}
|
kwargs.update({k: v for item in other for k, v in dict(item).items()})
|
||||||
super().update(*other, **kwargs)
|
setters: Dict[str, Any] = {
|
||||||
for attr, value in {**other_mapping, **kwargs}.items():
|
k: kwargs.pop(k)
|
||||||
|
for k in {**kwargs}.keys()
|
||||||
|
if k in self.__class__.__setters__
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in setters.items():
|
||||||
|
try:
|
||||||
|
super().__setattr__(key, value)
|
||||||
|
except AttributeError:
|
||||||
|
...
|
||||||
|
|
||||||
|
super().update(**kwargs)
|
||||||
|
for attr, value in {**setters, **kwargs}.items():
|
||||||
self._post_set(attr, value)
|
self._post_set(attr, value)
|
||||||
|
|
||||||
def _post_set(self, attr, value) -> None:
|
def _post_set(self, attr, value) -> None:
|
||||||
|
|
|
@ -4,8 +4,7 @@ from functools import partial, wraps
|
||||||
from inspect import getsource, signature
|
from inspect import getsource, signature
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import PurePath
|
from pathlib import Path, PurePath
|
||||||
from re import sub
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
|
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
|
||||||
|
@ -17,12 +16,7 @@ from sanic.base.meta import SanicMeta
|
||||||
from sanic.compat import stat_async
|
from sanic.compat import stat_async
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
||||||
from sanic.errorpages import RESPONSE_MAPPING
|
from sanic.errorpages import RESPONSE_MAPPING
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import ContentRangeError, FileNotFound, HeaderNotFound
|
||||||
ContentRangeError,
|
|
||||||
FileNotFound,
|
|
||||||
HeaderNotFound,
|
|
||||||
InvalidUsage,
|
|
||||||
)
|
|
||||||
from sanic.handlers import ContentRangeHandler
|
from sanic.handlers import ContentRangeHandler
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import deprecation, error_logger
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
|
@ -775,32 +769,40 @@ class RouteMixin(metaclass=SanicMeta):
|
||||||
content_type=None,
|
content_type=None,
|
||||||
__file_uri__=None,
|
__file_uri__=None,
|
||||||
):
|
):
|
||||||
# Using this to determine if the URL is trying to break out of the path
|
|
||||||
# served. os.path.realpath seems to be very slow
|
|
||||||
if __file_uri__ and "../" in __file_uri__:
|
|
||||||
raise InvalidUsage("Invalid URL")
|
|
||||||
# Merge served directory and requested file if provided
|
# Merge served directory and requested file if provided
|
||||||
# Strip all / that in the beginning of the URL to help prevent python
|
file_path_raw = Path(unquote(file_or_directory))
|
||||||
# from herping a derp and treating the uri as an absolute path
|
root_path = file_path = file_path_raw.resolve()
|
||||||
root_path = file_path = file_or_directory
|
not_found = FileNotFound(
|
||||||
if __file_uri__:
|
"File not found",
|
||||||
file_path = path.join(
|
path=file_or_directory,
|
||||||
file_or_directory, sub("^[/]*", "", __file_uri__)
|
relative_url=__file_uri__,
|
||||||
)
|
)
|
||||||
|
|
||||||
# URL decode the path sent by the browser otherwise we won't be able to
|
if __file_uri__:
|
||||||
# match filenames which got encoded (filenames with spaces etc)
|
# Strip all / that in the beginning of the URL to help prevent
|
||||||
file_path = path.abspath(unquote(file_path))
|
# python from herping a derp and treating the uri as an
|
||||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
# absolute path
|
||||||
error_logger.exception(
|
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
|
||||||
f"File not found: path={file_or_directory}, "
|
file_path_raw = Path(file_or_directory, unquoted_file_uri)
|
||||||
f"relative_url={__file_uri__}"
|
file_path = file_path_raw.resolve()
|
||||||
)
|
if (
|
||||||
raise FileNotFound(
|
file_path < root_path and not file_path_raw.is_symlink()
|
||||||
"File not found",
|
) or ".." in file_path_raw.parts:
|
||||||
path=file_or_directory,
|
error_logger.exception(
|
||||||
relative_url=__file_uri__,
|
f"File not found: path={file_or_directory}, "
|
||||||
)
|
f"relative_url={__file_uri__}"
|
||||||
|
)
|
||||||
|
raise not_found
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_path.relative_to(root_path)
|
||||||
|
except ValueError:
|
||||||
|
if not file_path_raw.is_symlink():
|
||||||
|
error_logger.exception(
|
||||||
|
f"File not found: path={file_or_directory}, "
|
||||||
|
f"relative_url={__file_uri__}"
|
||||||
|
)
|
||||||
|
raise not_found
|
||||||
try:
|
try:
|
||||||
headers = {}
|
headers = {}
|
||||||
# Check if the client has been sent this file before
|
# Check if the client has been sent this file before
|
||||||
|
@ -868,11 +870,7 @@ class RouteMixin(metaclass=SanicMeta):
|
||||||
except ContentRangeError:
|
except ContentRangeError:
|
||||||
raise
|
raise
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFound(
|
raise not_found
|
||||||
"File not found",
|
|
||||||
path=file_or_directory,
|
|
||||||
relative_url=__file_uri__,
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
error_logger.exception(
|
error_logger.exception(
|
||||||
f"Exception in static request handler: "
|
f"Exception in static request handler: "
|
||||||
|
|
|
@ -5,7 +5,7 @@ from os import environ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock, call
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -385,5 +385,24 @@ def test_config_set_methods(app, monkeypatch):
|
||||||
post_set.assert_called_once_with("FOO", 5)
|
post_set.assert_called_once_with("FOO", 5)
|
||||||
post_set.reset_mock()
|
post_set.reset_mock()
|
||||||
|
|
||||||
app.config.update_config({"FOO": 6})
|
app.config.update({"FOO": 6}, {"BAR": 7})
|
||||||
post_set.assert_called_once_with("FOO", 6)
|
post_set.assert_has_calls(
|
||||||
|
calls=[
|
||||||
|
call("FOO", 6),
|
||||||
|
call("BAR", 7),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
post_set.reset_mock()
|
||||||
|
|
||||||
|
app.config.update({"FOO": 8}, BAR=9)
|
||||||
|
post_set.assert_has_calls(
|
||||||
|
calls=[
|
||||||
|
call("FOO", 8),
|
||||||
|
call("BAR", 9),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
post_set.reset_mock()
|
||||||
|
|
||||||
|
app.config.update_config({"FOO": 10})
|
||||||
|
post_set.assert_called_once_with("FOO", 10)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from threading import Event
|
from threading import Event
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -77,6 +78,25 @@ def test_create_named_task(app):
|
||||||
app.run()
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
def test_named_task_called(app):
|
||||||
|
e = Event()
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
e.set()
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def isset(request):
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
return text(str(e.is_set()))
|
||||||
|
|
||||||
|
@app.before_server_start
|
||||||
|
async def setup(app, _):
|
||||||
|
app.add_task(coro, name="dummy_task")
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/")
|
||||||
|
assert response.body == b"True"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
|
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
|
||||||
def test_create_named_task_fails_outside_app(app):
|
def test_create_named_task_fails_outside_app(app):
|
||||||
async def dummy():
|
async def dummy():
|
||||||
|
|
|
@ -334,6 +334,22 @@ def test_config_fallback_before_and_after_startup(app):
|
||||||
assert response.content_type == "text/plain; charset=utf-8"
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_fallback_using_update_dict(app):
|
||||||
|
app.config.update({"FALLBACK_ERROR_FORMAT": "text"})
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/error")
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_fallback_using_update_kwarg(app):
|
||||||
|
app.config.update(FALLBACK_ERROR_FORMAT="text")
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/error")
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
def test_config_fallback_bad_value(app):
|
def test_config_fallback_bad_value(app):
|
||||||
message = "Unknown format: fake"
|
message = "Unknown format: fake"
|
||||||
with pytest.raises(SanicException, match=message):
|
with pytest.raises(SanicException, match=message):
|
||||||
|
|
|
@ -62,19 +62,15 @@ def test_streaming_body_requests(app):
|
||||||
|
|
||||||
data = ["hello", "world"]
|
data = ["hello", "world"]
|
||||||
|
|
||||||
class Data(AsyncByteStream):
|
|
||||||
def __init__(self, data):
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
async def __aiter__(self):
|
|
||||||
for value in self.data:
|
|
||||||
yield value.encode("utf-8")
|
|
||||||
|
|
||||||
client = ReusableClient(app, port=1234)
|
client = ReusableClient(app, port=1234)
|
||||||
|
|
||||||
|
async def stream(data):
|
||||||
|
for value in data:
|
||||||
|
yield value.encode("utf-8")
|
||||||
|
|
||||||
with client:
|
with client:
|
||||||
_, response1 = client.post("/", data=Data(data))
|
_, response1 = client.post("/", data=stream(data))
|
||||||
_, response2 = client.post("/", data=Data(data))
|
_, response2 = client.post("/", data=stream(data))
|
||||||
|
|
||||||
assert response1.status == response2.status == 200
|
assert response1.status == response2.status == 200
|
||||||
assert response1.json["data"] == response2.json["data"] == data
|
assert response1.json["data"] == response2.json["data"] == data
|
||||||
|
|
|
@ -4,8 +4,8 @@ from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic.server import loop
|
|
||||||
from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED
|
from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED
|
||||||
|
from sanic.server import loop
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -8,7 +9,7 @@ from time import gmtime, strftime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import text
|
from sanic import Sanic, text
|
||||||
from sanic.exceptions import FileNotFound
|
from sanic.exceptions import FileNotFound
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,6 +22,22 @@ def static_file_directory():
|
||||||
return static_directory
|
return static_directory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def double_dotted_directory_file(static_file_directory: str):
|
||||||
|
"""Generate double dotted directory and its files"""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
raise Exception("Windows doesn't support double dotted directories")
|
||||||
|
|
||||||
|
file_path = Path(static_file_directory) / "dotted.." / "dot.txt"
|
||||||
|
double_dotted_dir = file_path.parent
|
||||||
|
Path.mkdir(double_dotted_dir, exist_ok=True)
|
||||||
|
with open(file_path, "w") as f:
|
||||||
|
f.write("DOT\n")
|
||||||
|
yield file_path
|
||||||
|
Path.unlink(file_path)
|
||||||
|
Path.rmdir(double_dotted_dir)
|
||||||
|
|
||||||
|
|
||||||
def get_file_path(static_file_directory, file_name):
|
def get_file_path(static_file_directory, file_name):
|
||||||
return os.path.join(static_file_directory, file_name)
|
return os.path.join(static_file_directory, file_name)
|
||||||
|
|
||||||
|
@ -578,3 +595,43 @@ def test_resource_type_dir(app, static_file_directory):
|
||||||
def test_resource_type_unknown(app, static_file_directory, caplog):
|
def test_resource_type_unknown(app, static_file_directory, caplog):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
app.static("/static", static_file_directory, resource_type="unknown")
|
app.static("/static", static_file_directory, resource_type="unknown")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.platform == "win32",
|
||||||
|
reason="Windows does not support double dotted directories",
|
||||||
|
)
|
||||||
|
def test_dotted_dir_ok(
|
||||||
|
app: Sanic, static_file_directory: str, double_dotted_directory_file: Path
|
||||||
|
):
|
||||||
|
app.static("/foo", static_file_directory)
|
||||||
|
dot_relative_path = str(
|
||||||
|
double_dotted_directory_file.relative_to(static_file_directory)
|
||||||
|
)
|
||||||
|
_, response = app.test_client.get("/foo/" + dot_relative_path)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.body == b"DOT\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_breakout(app: Sanic, static_file_directory: str):
|
||||||
|
app.static("/foo", static_file_directory)
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/foo/..%2Ffake/server.py")
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/foo/..%2Fstatic/test.file")
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.platform != "win32", reason="Block backslash on Windows only"
|
||||||
|
)
|
||||||
|
def test_double_backslash_prohibited_on_win32(
|
||||||
|
app: Sanic, static_file_directory: str
|
||||||
|
):
|
||||||
|
app.static("/foo", static_file_directory)
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/foo/static/..\\static/test.file")
|
||||||
|
assert response.status == 404
|
||||||
|
_, response = app.test_client.get("/foo/static\\../static/test.file")
|
||||||
|
assert response.status == 404
|
||||||
|
|
Loading…
Reference in New Issue
Block a user