Skip to content

Commit

Permalink
Merge branch 'master' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
graingert committed Sep 6, 2020
2 parents f5741ae + b95acea commit 79825b5
Show file tree
Hide file tree
Showing 29 changed files with 169 additions and 59 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/publish.yml
Expand Up @@ -18,7 +18,9 @@ jobs:
python-version: 3.7
- name: "Install dependencies"
run: "scripts/install"
- name: "Publish"
- name: "Build package & docs"
run: "scripts/build"
- name: "Publish to PyPI & deploy docs"
run: "scripts/publish"
env:
TWINE_USERNAME: __token__
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/test-suite.yml
Expand Up @@ -2,6 +2,8 @@
name: Test Suite

on:
push:
branches: ["master"]
pull_request:
branches: ["master"]

Expand All @@ -21,5 +23,9 @@ jobs:
python-version: "${{ matrix.python-version }}"
- name: "Install dependencies"
run: "scripts/install"
- name: "Run linting checks"
run: "scripts/check"
- name: "Build package & docs"
run: "scripts/build"
- name: "Run tests"
run: "scripts/test"
10 changes: 8 additions & 2 deletions .gitignore
Expand Up @@ -3,5 +3,11 @@ test.db
.coverage
.pytest_cache/
.mypy_cache/
starlette.egg-info/
venv/
__pycache__/
htmlcov/
site/
*.egg-info/
venv*/
.python-version
build/
dist/
7 changes: 6 additions & 1 deletion requirements.txt
Expand Up @@ -10,8 +10,9 @@ ujson

# Testing
autoflake
black
black==20.8b1
databases[sqlite]
flake8
isort==5.*
mypy
pytest
Expand All @@ -22,3 +23,7 @@ pytest-asyncio
mkdocs
mkdocs-material
mkautodoc

# Packaging
twine
wheel
5 changes: 4 additions & 1 deletion scripts/README.md
Expand Up @@ -2,7 +2,10 @@

* `scripts/install` - Install dependencies in a virtual environment.
* `scripts/test` - Run the test suite.
* `scripts/lint` - Run the code linting.
* `scripts/lint` - Run the automated code linting/formatting tools.
* `scripts/check` - Run the code linting, checking that it passes.
* `scripts/coverage` - Check that code coverage is complete.
* `scripts/build` - Build source and wheel packages.
* `scripts/publish` - Publish the latest version to PyPI.

Styled after GitHub's ["Scripts to Rule Them All"](https://github.com/github/scripts-to-rule-them-all).
13 changes: 13 additions & 0 deletions scripts/build
@@ -0,0 +1,13 @@
#!/bin/sh -e

if [ -d 'venv' ] ; then
PREFIX="venv/bin/"
else
PREFIX=""
fi

set -x

${PREFIX}python setup.py sdist bdist_wheel
${PREFIX}twine check dist/*
${PREFIX}mkdocs build
14 changes: 14 additions & 0 deletions scripts/check
@@ -0,0 +1,14 @@
#!/bin/sh -e

export PREFIX=""
if [ -d 'venv' ] ; then
export PREFIX="venv/bin/"
fi
export SOURCE_FILES="starlette tests"

set -x

${PREFIX}isort --check --diff --project=starlette $SOURCE_FILES
${PREFIX}black --check --diff $SOURCE_FILES
${PREFIX}flake8 $SOURCE_FILES
${PREFIX}mypy $SOURCE_FILES
10 changes: 10 additions & 0 deletions scripts/coverage
@@ -0,0 +1,10 @@
#!/bin/sh -e

export PREFIX=""
if [ -d 'venv' ] ; then
export PREFIX="venv/bin/"
fi

set -x

${PREFIX}coverage report --show-missing --skip-covered --fail-under=100
10 changes: 10 additions & 0 deletions scripts/docs
@@ -0,0 +1,10 @@
#!/bin/sh -e

export PREFIX=""
if [ -d 'venv' ] ; then
export PREFIX="venv/bin/"
fi

set -x

${PREFIX}mkdocs serve
9 changes: 4 additions & 5 deletions scripts/lint
Expand Up @@ -4,11 +4,10 @@ export PREFIX=""
if [ -d 'venv' ] ; then
export PREFIX="venv/bin/"
fi
export SOURCE_FILES="starlette tests"

set -x

${PREFIX}mypy starlette --ignore-missing-imports --disallow-untyped-defs
${PREFIX}autoflake --in-place --recursive starlette tests setup.py
${PREFIX}black starlette tests setup.py
${PREFIX}isort --profile=black --combine-as starlette tests setup.py
${PREFIX}mypy starlette --ignore-missing-imports --disallow-untyped-defs
${PREFIX}autoflake --in-place --recursive $SOURCE_FILES
${PREFIX}isort --project=starlette $SOURCE_FILES
${PREFIX}black $SOURCE_FILES
3 changes: 0 additions & 3 deletions scripts/publish
@@ -1,7 +1,6 @@
#!/bin/sh -e

VERSION_FILE="starlette/__init__.py"
PYTHONPATH=.

if [ -d 'venv' ] ; then
PREFIX="venv/bin/"
Expand All @@ -23,7 +22,5 @@ fi

set -x

${PREFIX}pip install twine wheel mkdocs mkdocs-material mkautodoc
${PREFIX}python setup.py sdist bdist_wheel
${PREFIX}twine upload dist/*
${PREFIX}mkdocs gh-deploy --force
19 changes: 10 additions & 9 deletions scripts/test
@@ -1,17 +1,18 @@
#!/bin/sh -e
#!/bin/sh

export PREFIX=""
if [ -d 'venv' ] ; then
export PREFIX="venv/bin/"
fi

export VERSION_SCRIPT="import sys; print('%s.%s' % sys.version_info[0:2])"
export PYTHON_VERSION=`python -c "$VERSION_SCRIPT"`
set -ex

set -x
if [ -z $GITHUB_ACTIONS ]; then
scripts/check
fi

${PREFIX}pytest $@

PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov-config tests/.ignore_lifespan -W ignore::DeprecationWarning --cov=starlette --cov=tests --cov-fail-under=100 --cov-report=term-missing ${@}
${PREFIX}mypy starlette --ignore-missing-imports --disallow-untyped-defs
${PREFIX}autoflake --recursive starlette tests setup.py
${PREFIX}isort --profile=black --combine-as --check starlette tests setup.py
${PREFIX}black starlette tests setup.py --check
if [ -z $GITHUB_ACTIONS ]; then
scripts/coverage
fi
19 changes: 19 additions & 0 deletions setup.cfg
@@ -0,0 +1,19 @@
[flake8]
ignore = W503, E203, B305
max-line-length = 88

[mypy]
disallow_untyped_defs = True
ignore_missing_imports = True

[mypy-tests.*]
disallow_untyped_defs = False
# https://github.com/encode/starlette/issues/1045
# check_untyped_defs = True

[tool:isort]
profile = black
combine_as_imports = True

[tool:pytest]
addopts = --cov-report= --cov=starlette --cov=tests -rxXs
9 changes: 5 additions & 4 deletions starlette/applications.py
Expand Up @@ -25,8 +25,9 @@ class Starlette:
with handled exception cases occuring in the routing or endpoints.
* **exception_handlers** - A dictionary mapping either integer status codes,
or exception class types onto callables which handle the exceptions.
Exception handler callables should be of the form `handler(request, exc) -> response`
and may be be either standard functions, or async functions.
Exception handler callables should be of the form
`handler(request, exc) -> response` and may be be either standard functions, or
async functions.
* **on_startup** - A list of callables to run on application startup.
Startup handler callables do not take any arguments, and may be be either
standard functions, or async functions.
Expand Down Expand Up @@ -76,11 +77,11 @@ def build_middleware_stack(self) -> ASGIApp:
exception_handlers[key] = value

middleware = (
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug,)]
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
+ self.user_middleware
+ [
Middleware(
ExceptionMiddleware, handlers=exception_handlers, debug=debug,
ExceptionMiddleware, handlers=exception_handlers, debug=debug
)
]
)
Expand Down
12 changes: 7 additions & 5 deletions starlette/config.py
Expand Up @@ -24,14 +24,16 @@ def __getitem__(self, key: typing.Any) -> typing.Any:
def __setitem__(self, key: typing.Any, value: typing.Any) -> None:
if key in self._has_been_read:
raise EnvironError(
f"Attempting to set environ['{key}'], but the value has already been read."
f"Attempting to set environ['{key}'], but the value has already been "
"read."
)
self._environ.__setitem__(key, value)

def __delitem__(self, key: typing.Any) -> None:
if key in self._has_been_read:
raise EnvironError(
f"Attempting to delete environ['{key}'], but the value has already been read."
f"Attempting to delete environ['{key}'], but the value has already "
"been read."
)
self._environ.__delitem__(key)

Expand All @@ -57,12 +59,12 @@ def __init__(
self.file_values = self._read_file(env_file)

def __call__(
self, key: str, cast: typing.Callable = None, default: typing.Any = undefined,
self, key: str, cast: typing.Callable = None, default: typing.Any = undefined
) -> typing.Any:
return self.get(key, cast, default)

def get(
self, key: str, cast: typing.Callable = None, default: typing.Any = undefined,
self, key: str, cast: typing.Callable = None, default: typing.Any = undefined
) -> typing.Any:
if key in self.environ:
value = self.environ[key]
Expand All @@ -87,7 +89,7 @@ def _read_file(self, file_name: typing.Union[str, Path]) -> typing.Dict[str, str
return file_values

def _perform_cast(
self, key: str, value: typing.Any, cast: typing.Callable = None,
self, key: str, value: typing.Any, cast: typing.Callable = None
) -> typing.Any:
if cast is None or value is None:
return value
Expand Down
2 changes: 1 addition & 1 deletion starlette/graphql.py
Expand Up @@ -275,4 +275,4 @@ async def handle_graphiql(self, request: Request) -> Response:
</script>
</body>
</html>
"""
""" # noqa: E501
4 changes: 2 additions & 2 deletions starlette/middleware/cors.py
Expand Up @@ -106,8 +106,8 @@ def preflight_response(self, request_headers: Headers) -> Response:

if self.is_allowed_origin(origin=requested_origin):
if not self.allow_all_origins:
# If self.allow_all_origins is True, then the "Access-Control-Allow-Origin"
# header is already set to "*".
# If self.allow_all_origins is True, then the
# "Access-Control-Allow-Origin" header is already set to "*".
# If we only allow specific origins, then we have to mirror back
# the Origin header in the response.
headers["Access-Control-Allow-Origin"] = requested_origin
Expand Down
13 changes: 9 additions & 4 deletions starlette/middleware/errors.py
Expand Up @@ -109,7 +109,7 @@
</p>
<div id="{frame_filename}-{frame_lineno}" class="source-code {collapsed}">{code_context}</div>
</div>
"""
""" # noqa: E501

LINE = """
<p><span class="frame-line">
Expand Down Expand Up @@ -200,10 +200,12 @@ def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> s
)

values = {
# HTML escape - filename could contain < or >, especially if it's a virtual file e.g. <stdin> in the REPL
# HTML escape - filename could contain < or >, especially if it's a virtual
# file e.g. <stdin> in the REPL
"frame_filename": html.escape(frame.filename),
"frame_lineno": frame.lineno,
# HTML escape - if you try very hard it's possible to name a function with < or >
# HTML escape - if you try very hard it's possible to name a function with <
# or >
"frame_name": html.escape(frame.function),
"code_context": code_context,
"collapsed": "collapsed" if is_collapsed else "",
Expand All @@ -226,7 +228,10 @@ def generate_html(self, exc: Exception, limit: int = 7) -> str:
is_collapsed = True

# escape error class and text
error = f"{html.escape(traceback_obj.exc_type.__name__)}: {html.escape(str(traceback_obj))}"
error = (
f"{html.escape(traceback_obj.exc_type.__name__)}: "
f"{html.escape(str(traceback_obj))}"
)

return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html)

Expand Down
3 changes: 2 additions & 1 deletion starlette/middleware/wsgi.py
Expand Up @@ -44,7 +44,8 @@ def build_environ(scope: Scope, body: bytes) -> dict:
corrected_name = "CONTENT_TYPE"
else:
corrected_name = f"HTTP_{name}".upper().replace("-", "_")
# HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case
# HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in
# case
value = value.decode("latin1")
if corrected_name in environ:
value = environ[corrected_name] + "," + value
Expand Down
3 changes: 2 additions & 1 deletion starlette/requests.py
Expand Up @@ -156,7 +156,8 @@ def state(self) -> State:
if not hasattr(self, "_state"):
# Ensure 'state' has an empty dict if it's not already populated.
self.scope.setdefault("state", {})
# Create a state instance with a reference to the dict in which it should store info
# Create a state instance with a reference to the dict in which it should
# store info
self._state = State(self.scope["state"])
return self._state

Expand Down
6 changes: 3 additions & 3 deletions starlette/routing.py
Expand Up @@ -520,20 +520,20 @@ async def lifespan(self, scope: Scope, receive: Receive, send: Send) -> None:
"""
first = True
app = scope.get("app")
message = await receive()
await receive()
try:
if inspect.isasyncgenfunction(self.lifespan_context):
async for item in self.lifespan_context(app):
assert first, "Lifespan context yielded multiple times."
first = False
await send({"type": "lifespan.startup.complete"})
message = await receive()
await receive()
else:
for item in self.lifespan_context(app): # type: ignore
assert first, "Lifespan context yielded multiple times."
first = False
await send({"type": "lifespan.startup.complete"})
message = await receive()
await receive()
except BaseException:
if first:
exc_text = traceback.format_exc()
Expand Down
3 changes: 2 additions & 1 deletion starlette/staticfiles.py
Expand Up @@ -147,7 +147,8 @@ async def lookup_path(
full_path = os.path.realpath(os.path.join(directory, path))
directory = os.path.realpath(directory)
if os.path.commonprefix([full_path, directory]) != directory:
# Don't allow misbehaving clients to break out of the static files directory.
# Don't allow misbehaving clients to break out of the static files
# directory.
continue
try:
stat_result = await aio_stat(full_path)
Expand Down
2 changes: 1 addition & 1 deletion tests/middleware/test_cors.py
Expand Up @@ -181,7 +181,7 @@ def test_cors_allow_origin_regex_fullmatch():
app.add_middleware(
CORSMiddleware,
allow_headers=["X-Example", "Content-Type"],
allow_origin_regex="https://.*\.example.org",
allow_origin_regex=r"https://.*\.example.org",
)

@app.route("/")
Expand Down
2 changes: 1 addition & 1 deletion tests/middleware/test_lifespan.py
Expand Up @@ -24,7 +24,7 @@ def run_shutdown():
app = Router(
on_startup=[run_startup],
on_shutdown=[run_shutdown],
routes=[Route("/", hello_world),],
routes=[Route("/", hello_world)],
)

assert not startup_complete
Expand Down

0 comments on commit 79825b5

Please sign in to comment.