Skip to content

Commit

Permalink
Add tests to the Python build, wheel, and Mypyc packages.
Browse files Browse the repository at this point in the history
Create shared tests to be used for both unit and integration
testing, and increase the coverage of unit tests to 100%. Additionally,
before publishing the pre-release, run the shared tests against the
build version for each OS supported by the code.

The intention here is to ensure that all build packages are tested
and function correctly for all supported features.

Resolves: #577
  • Loading branch information
nycholas committed Oct 26, 2024
1 parent dd8c991 commit fc1b849
Show file tree
Hide file tree
Showing 83 changed files with 5,215 additions and 4,995 deletions.
5 changes: 3 additions & 2 deletions Dockerfile.it
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ WORKDIR /svc
COPY requirements/tests.txt /svc/
RUN pip install pip setuptools wheel --upgrade \
&& pip wheel --wheel-dir=/svc/wheels -r tests.txt \
poetry-core>=1.0.0
&& pip install poetry-core>=1.0.0

FROM python:3.12-alpine

Expand Down Expand Up @@ -57,6 +57,7 @@ ARG VERSION=1
RUN echo "Version: ${VERSION}"

COPY .docker/* requirements/tests.txt tests/integration/*.py tests/integration/*.ini /app/
COPY tests/integration/shared/*.py tests/shared/ /app/shared/

RUN pip install pip setuptools wheel --upgrade \
&& pip install --no-index --find-links=/svc/wheels -r tests.txt \
Expand All @@ -74,4 +75,4 @@ RUN pip install pip setuptools wheel --upgrade \

USER flask_user

CMD ./wait-for.sh ${SITE_DOMAIN}:${SITE_PORT} -t 600 -- pytest --junitxml=test-results/junit.xml
CMD ./wait-for.sh ${SITE_DOMAIN}:${SITE_PORT} -t 600 -- pytest -n auto --junitxml=test-results/junit.xml
4 changes: 1 addition & 3 deletions Dockerfile.local
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,4 @@ USER flask_user
ARG VERSION=1
RUN echo "Version: ${VERSION}"

COPY .docker/* /app/
COPY tests/test_apps/app/__init__.py /app/app.py
COPY tests/test_apps/async_app/__init__.py /app/async_app.py
COPY .docker/* tests/test_apps/ /app/
2 changes: 1 addition & 1 deletion Dockerfile.py310.test
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN set -ex \
&& pip install -r requirements/base.txt \
&& pip install -r requirements/style.txt \
&& pip install -r requirements/tests.txt \
poetry-core>=1.0.0 \
&& pip install poetry-core>=1.0.0 \
&& apk del .build-deps \
&& addgroup -S kuchulu \
&& adduser \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.py311.test
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN set -ex \
&& pip install -r requirements/base.txt \
&& pip install -r requirements/style.txt \
&& pip install -r requirements/tests.txt \
poetry-core>=1.0.0 \
&& pip install poetry-core>=1.0.0 \
&& apk del .build-deps \
&& addgroup -S kuchulu \
&& adduser \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.py312.test
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN set -ex \
&& pip install -r requirements/base.txt \
&& pip install -r requirements/style.txt \
&& pip install -r requirements/tests.txt \
poetry-core>=1.0.0 \
&& pip install poetry-core>=1.0.0 \
&& apk del .build-deps \
&& addgroup -S kuchulu \
&& adduser \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.py38.test
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN set -ex \
&& pip install -r requirements/base.txt \
&& pip install -r requirements/style.txt \
&& pip install -r requirements/tests.txt \
poetry-core>=1.0.0 \
&& pip install poetry-core>=1.0.0 \
&& apk del .build-deps \
&& addgroup -S kuchulu \
&& adduser \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.py39.test
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN set -ex \
&& pip install -r requirements/base.txt \
&& pip install -r requirements/style.txt \
&& pip install -r requirements/tests.txt \
poetry-core>=1.0.0 \
&& pip install poetry-core>=1.0.0 \
&& apk del .build-deps \
&& addgroup -S kuchulu \
&& adduser \
Expand Down
8 changes: 5 additions & 3 deletions docker-compose.it.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ services:
- FLASK_SERVER_NAME=app:5000
user: ${UID:-0}:${GID:-0}
command: >
python app.py
python -m app
ports:
- '5000:5000'
networks:
Expand All @@ -45,6 +45,8 @@ services:
- API_URL=https://async-app.flask-jsonrpc.cenobit.es/api
- BROWSABLE_API_URL=https://async-app.flask-jsonrpc.cenobit.es/api/browse
user: ${UID:-0}:${GID:-0}
command: >
./wait-for.sh ${SITE_DOMAIN}:${SITE_PORT} -t 600 -- pytest -n auto --junitxml=test-results/junit.xml test_async_app.py
volumes:
- .pytest_cache/test-results/async-app:/app/test-results
- .pytest_cache/screnshots/async-app:/app/.pytest_cache/screnshots
Expand All @@ -63,7 +65,7 @@ services:
- FLASK_SERVER_NAME=async-app:5000
user: ${UID:-0}:${GID:-0}
command: >
python async-app.py
python -m async_app
ports:
- '5001:5000'
networks:
Expand Down Expand Up @@ -99,7 +101,7 @@ services:
- FLASK_SERVER_NAME=mypyc-app:5000
user: ${UID:-0}:${GID:-0}
command: >
python app.py
python -m app
ports:
- '5002:5000'
networks:
Expand Down
2 changes: 1 addition & 1 deletion examples/javascript/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ deps =
pytest
async: Flask[async]>=3.0.0,<4.0
commands =
pytest -vv --tb=short --basetemp={envtmpdir} {posargs}
pytest -n auto -vv --tb=short --basetemp={envtmpdir} {posargs}
2 changes: 1 addition & 1 deletion examples/minimal-async/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ deps =
pytest
async: Flask[async]>=3.0.0,<4.0
commands =
pytest -vv --tb=short --basetemp={envtmpdir} {posargs}
pytest -n auto -vv --tb=short --basetemp={envtmpdir} {posargs}
2 changes: 1 addition & 1 deletion examples/minimal/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ deps =
pytest
async: Flask[async]>=3.0.0,<4.0
commands =
pytest -vv --tb=short --basetemp={envtmpdir} {posargs}
pytest -n auto -vv --tb=short --basetemp={envtmpdir} {posargs}
2 changes: 1 addition & 1 deletion examples/modular/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ deps =
pytest
async: Flask[async]>=3.0.0,<4.0
commands =
pytest -vv --tb=short --basetemp={envtmpdir} {posargs}
pytest -n auto -vv --tb=short --basetemp={envtmpdir} {posargs}
12 changes: 12 additions & 0 deletions examples/multiplesite/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
# POSSIBILITY OF SUCH DAMAGE.
import typing as t

import pytest
from multiplesite.app import UnauthorizedError

if t.TYPE_CHECKING:
from flask.testing import FlaskClient

Expand All @@ -46,6 +49,15 @@ def test_index_v2(client: 'FlaskClient') -> None:
assert rv.status_code == 200


def test_index_v2_with_invalid_auth(client: 'FlaskClient') -> None:
with pytest.raises(UnauthorizedError):
client.post(
'/api/v2',
json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.index'},
headers={'X-Username': 'username', 'X-Password': 'invalid'},
)


def test_rpc_describe_v1(client: 'FlaskClient') -> None:
rv = client.post('/api/v1', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'})
data = rv.get_json()
Expand Down
2 changes: 1 addition & 1 deletion examples/multiplesite/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ deps =
pytest
async: Flask[async]>=3.0.0,<4.0
commands =
pytest -vv --tb=short --basetemp={envtmpdir} {posargs}
pytest -n auto -vv --tb=short --basetemp={envtmpdir} {posargs}
2 changes: 1 addition & 1 deletion examples/openrpc/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ deps =
pytest
async: Flask[async]>=3.0.0,<4.0
commands =
pytest -vv --tb=short --basetemp={envtmpdir} {posargs}
pytest -n auto -vv --tb=short --basetemp={envtmpdir} {posargs}
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ inline-quotes = "single"
docstring-quotes = "double"

[tool.ruff.lint.flake8-type-checking]
exempt-modules = ["typing", "typing_extensions"]
exempt-modules = ["typing", "typing_extensions", "annotated_types"]

[tool.ruff.lint.flake8-bandit]
check-typed-exception = true
Expand Down Expand Up @@ -149,7 +149,7 @@ section-order = [
[tool.ruff.lint.isort.sections]
"flask" = ["flask"]
"pydantic" = ["pydantic"]
"typing-extensions" = ["typing_extensions"]
"typing-extensions" = ["typing_inspect", "typing_extensions"]

[tool.ruff.lint.pydocstyle]
convention = "google"
Expand All @@ -175,6 +175,7 @@ filterwarnings = [
"ignore::pytest.PytestUnraisableExceptionWarning"
]
norecursedirs = [
"tests/shared",
"tests/test_apps",
"tests/integration",
]
Expand Down Expand Up @@ -244,6 +245,7 @@ module = [
"asgiref.*",
"mypy-werkzeug.datastructures.*",
"typeguard.*",
"typing_inspect.*",
"dotenv.*",
]
ignore_missing_imports = true
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def find_python_files(path: pathlib.Path) -> t.List[pathlib.Path]:
setup_attrs = {'name': 'Flask-JSONRPC', 'packages': setuptools.find_packages()}

if USE_MYPYC:
from mypyc.build import mypycify # pylint: disable=E0611
from mypyc.build import mypycify

project_dir = pathlib.Path(__file__).resolve().parent

Expand Down
6 changes: 3 additions & 3 deletions src/flask_jsonrpc/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ def __init__(
if app:
self.init_app(app)

def _make_jsonrpc_browse_url(self: Self, path: str) -> str:
return ''.join([path.rstrip('/'), '/browse'])

def get_jsonrpc_site(self: Self) -> JSONRPCSite:
return self.jsonrpc_site

def get_jsonrpc_site_api(self: Self) -> type[JSONRPCView]:
return self.jsonrpc_site_api

def _make_jsonrpc_browse_url(self: Self, path: str) -> str:
return ''.join([path.rstrip('/'), '/browse'])

def init_app(self: Self, app: Flask) -> None:
http_host = app.config.get('SERVER_NAME')
app_root = app.config['APPLICATION_ROOT']
Expand Down
8 changes: 8 additions & 0 deletions src/flask_jsonrpc/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ def get_jsonrpc_site(self: Self) -> JSONRPCSite:

def get_jsonrpc_site_api(self: Self) -> type[JSONRPCView]:
return self.jsonrpc_site_api

def register(
self: Self,
view_func: t.Callable[..., t.Any],
name: str | None = None,
**options: t.Any, # noqa: ANN401
) -> None:
self.register_view_function(view_func, name, **options)
50 changes: 25 additions & 25 deletions src/flask_jsonrpc/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
# Added in version 3.11.
from typing_extensions import Self

from . import typing as fjt # pylint: disable=W0404
from . import typing as fjt
from .helpers import from_python_type

if t.TYPE_CHECKING:
Expand All @@ -48,50 +48,35 @@ def __init__(self: Self, jsonrpc_site: JSONRPCSite) -> None:
self.jsonrpc_site = jsonrpc_site
self.register(jsonrpc_site)

def register(self: Self, jsonrpc_site: JSONRPCSite) -> None:
def describe() -> fjt.ServiceDescribe:
return self.service_describe()

fn_annotations = {'return': fjt.ServiceDescribe}
setattr(describe, 'jsonrpc_method_name', JSONRPC_DESCRIBE_METHOD_NAME) # noqa: B010
setattr(describe, 'jsonrpc_method_sig', fn_annotations) # noqa: B010
setattr(describe, 'jsonrpc_method_return', fn_annotations.pop('return', None)) # noqa: B010
setattr(describe, 'jsonrpc_method_params', fn_annotations) # noqa: B010
setattr(describe, 'jsonrpc_validate', True) # noqa: B010
setattr(describe, 'jsonrpc_notification', False) # noqa: B010
setattr(describe, 'jsonrpc_options', {}) # noqa: B010
jsonrpc_site.register(JSONRPC_DESCRIBE_METHOD_NAME, describe)
self.describe = describe

def python_type_name(self: Self, pytype: t.Any) -> str: # noqa: ANN401
def _python_type_name(self: Self, pytype: t.Any) -> str: # noqa: ANN401
return str(from_python_type(pytype))

def service_method_params_desc(
def _service_method_params_desc(
self: Self, view_func: t.Callable[..., t.Any]
) -> list[fjt.ServiceMethodParamsDescribe]:
return [
fjt.ServiceMethodParamsDescribe(name=name, type=self.python_type_name(tp))
fjt.ServiceMethodParamsDescribe(name=name, type=self._python_type_name(tp))
for name, tp in getattr(view_func, 'jsonrpc_method_params', {}).items()
]

def service_methods_desc(self: Self) -> t.OrderedDict[str, fjt.ServiceMethodDescribe]:
def _service_methods_desc(self: Self) -> t.OrderedDict[str, fjt.ServiceMethodDescribe]:
methods: t.OrderedDict[str, fjt.ServiceMethodDescribe] = OrderedDict()
for key, view_func in self.jsonrpc_site.view_funcs.items():
name = getattr(view_func, 'jsonrpc_method_name', key)
method = fjt.ServiceMethodDescribe(
type=JSONRPC_DESCRIBE_SERVICE_METHOD_TYPE,
options=getattr(view_func, 'jsonrpc_options', {}),
params=self.service_method_params_desc(view_func),
params=self._service_method_params_desc(view_func),
returns=fjt.ServiceMethodReturnsDescribe(
type=self.python_type_name(getattr(view_func, 'jsonrpc_method_return', type(None)))
type=self._python_type_name(getattr(view_func, 'jsonrpc_method_return', type(None)))
),
)
# mypyc: pydantic optional value
method.description = getattr(view_func, '__doc__', None)
methods[name] = method
return methods

def service_server_url(self: Self) -> str:
def _service_server_url(self: Self) -> str:
url = urlsplit(self.jsonrpc_site.base_url or self.jsonrpc_site.path)
return (
f"{url.scheme!r}://{url.netloc!r}/{(self.jsonrpc_site.path or '').lstrip('/')}"
Expand All @@ -104,9 +89,24 @@ def service_describe(self: Self) -> fjt.ServiceDescribe:
id=f'urn:uuid:{self.jsonrpc_site.uuid}',
version=self.jsonrpc_site.version,
name=self.jsonrpc_site.name,
servers=[fjt.ServiceServersDescribe(url=self.service_server_url())], # pytype: disable=missing-parameter
methods=self.service_methods_desc(),
servers=[fjt.ServiceServersDescribe(url=self._service_server_url())], # pytype: disable=missing-parameter
methods=self._service_methods_desc(),
)
# mypyc: pydantic optional value
serv_desc.description = self.jsonrpc_site.__doc__
return serv_desc

def register(self: Self, jsonrpc_site: JSONRPCSite) -> None:
def describe() -> fjt.ServiceDescribe:
return self.service_describe()

fn_annotations = {'return': fjt.ServiceDescribe}
setattr(describe, 'jsonrpc_method_name', JSONRPC_DESCRIBE_METHOD_NAME) # noqa: B010
setattr(describe, 'jsonrpc_method_sig', fn_annotations) # noqa: B010
setattr(describe, 'jsonrpc_method_return', fn_annotations.pop('return', None)) # noqa: B010
setattr(describe, 'jsonrpc_method_params', fn_annotations) # noqa: B010
setattr(describe, 'jsonrpc_validate', True) # noqa: B010
setattr(describe, 'jsonrpc_notification', False) # noqa: B010
setattr(describe, 'jsonrpc_options', {}) # noqa: B010
jsonrpc_site.register(JSONRPC_DESCRIBE_METHOD_NAME, describe)
self.describe = describe
4 changes: 3 additions & 1 deletion src/flask_jsonrpc/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
from pydantic.main import BaseModel


def serializable(obj: t.Any) -> t.Any: # noqa: ANN401
def serializable(obj: t.Any) -> t.Any: # noqa: ANN401, C901
if isinstance(obj, (bytes, bytearray)):
return obj.decode('utf-8')
if isinstance(obj, Enum):
return obj.value
if isinstance(obj, PurePath):
Expand Down
2 changes: 1 addition & 1 deletion src/flask_jsonrpc/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

_("You're lazy...") # this function lazy-loads settings (pragma: no cover)
except (ImportError, NameError):
_ = lambda t, *a, **k: t # noqa: E731 pylint: disable=C3001
_ = lambda t, *a, **k: t # noqa: E731


class JSONRPCError(Exception):
Expand Down
Loading

0 comments on commit fc1b849

Please sign in to comment.