Skip to content

Commit

Permalink
Merge pull request #27 from endlessm/python-3.11
Browse files Browse the repository at this point in the history
Use Python 3.11
  • Loading branch information
wjt authored May 23, 2024
2 parents a61cc09 + 5879080 commit b8803b4
Show file tree
Hide file tree
Showing 14 changed files with 1,175 additions and 611 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ jobs:
name: image
- name: Load Docker Image
run: docker load < docker-image.tar
- name: Lint and Type Checking
- name: Lint
run: docker run --rm --entrypoint="" amp pipenv run lint
- name: Type Checking
run: docker run --rm --entrypoint="" amp pipenv run type-check

push:
name: Upload the Docker image to Docker Hub
Expand Down
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ fixing your issue as quickly as possible.

The tools required to work on the Azafea metrics proxy are the following:

* Python >= 3.7, with Pip
* [pipenv](https://docs.pipenv.org/)
* Python >= 3.11, with Pip
* [Pipenv](https://docs.pipenv.org/) >= 2023.10.24

Pipenv is not strictly mandatory to contribute to the Azafea metrics proxy, you
can use any way you prefer to manage the dependencies. We do recommend using it
Expand Down Expand Up @@ -69,10 +69,11 @@ community.
We also use type checking with [mypy](http://www.mypy-lang.org/), which
prevents a lot of problems inherent to dynamically typed languages like Python.

Both are run automatically with the following command:
Both are run with the following commands:

```
$ pipenv run lint
$ pipenv run type-check
```


Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.7-alpine
FROM python:3.11-alpine

RUN pip install --no-cache-dir pipenv template && \
apk add --update --no-cache git build-base
Expand Down
16 changes: 12 additions & 4 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
[pipenv]
sort_pipfile = true

[requires]
python_version = "3.11"

[dev-packages]
flake8 = "*"
flake8-bugbear = "*"
freezegun = "*"
mypy = ">=0.720"
pytest = "*"
pytest-aiohttp = "*"
pytest-asyncio = "*"
pytest-cov = "*"
pytest-flake8 = "*"
pytest-mypy = ">=0.3.3"
types-toml = "*"

[packages]
aiohttp = ">= 3.5.0"
aioredis = "~= 1.3"
pydantic = ">= 1.1"
pydantic = "~= 2.7"
toml = "*"

[scripts]
lint = "py.test --flake8 --mypy -m 'flake8 or mypy'"
lint = "flake8"
proxy = "python -m azafea_metrics_proxy"
test = "py.test --cov=azafea_metrics_proxy --cov-fail-under=99 --no-cov-on-fail -m 'not integration'"
test-all = "py.test --cov=azafea_metrics_proxy --cov-fail-under=99 --no-cov-on-fail"
type-check = "mypy"
1,652 changes: 1,090 additions & 562 deletions Pipfile.lock

Large diffs are not rendered by default.

34 changes: 24 additions & 10 deletions azafea_metrics_proxy/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
import dataclasses
import logging
import os
from typing import Any, Dict, List, MutableMapping
from typing import Any, List, MutableMapping

from pydantic.class_validators import validator
from pydantic import field_validator, ValidationError
from pydantic.dataclasses import dataclass
from pydantic.error_wrappers import ValidationError
from pydantic_core import ErrorDetails

import toml

Expand All @@ -40,18 +40,32 @@


class InvalidConfigurationError(Exception):
def __init__(self, errors: List[Dict[str, Any]]) -> None:
def __init__(self, errors: List[ErrorDetails]) -> None:
self.errors = errors

def __str__(self) -> str:
msg = ['Invalid configuration:']

for e in self.errors:
loc = e['loc']
msg.append(f"* {'.'.join(loc)}: {e['msg']}")
loc = self._loc_str(e)
msg.append(f"* {loc}: {e['msg']}")

return '\n'.join(msg)

# The loc field in ErrorDetails is a tuple of strings and ints.
# https://docs.pydantic.dev/latest/errors/errors/#customize-error-messages
@staticmethod
def _loc_str(details: ErrorDetails) -> str:
path = ''
for i, element in enumerate(details['loc']):
if isinstance(element, str):
if i > 0:
path += '.'
path += element
else: # pragma: no cover (our config does not use lists)
path += f'[{element}]'
return path


class NoSuchConfigurationError(AttributeError):
pass
Expand All @@ -66,7 +80,7 @@ def __getattr__(self, name: str) -> Any:
class Main(_Base):
verbose: bool = False

@validator('verbose', pre=True)
@field_validator('verbose', mode='before')
def verbose_is_boolean(cls, value: Any) -> bool:
return is_boolean(value)

Expand All @@ -78,11 +92,11 @@ class Redis(_Base):
password: str = DEFAULT_PASSWORD
ssl: bool = False

@validator('host', pre=True)
@field_validator('host', mode='before')
def host_is_non_empty_string(cls, value: Any) -> str:
return is_non_empty_string(value)

@validator('port', pre=True)
@field_validator('port', mode='before')
def port_is_strictly_positive_integer(cls, value: Any) -> int:
return is_strictly_positive_integer(value)

Expand All @@ -92,7 +106,7 @@ class Config(_Base):
main: Main = dataclasses.field(default_factory=Main)
redis: Redis = dataclasses.field(default_factory=Redis)

def __post_init_post_parse__(self) -> None:
def __post_init__(self) -> None:
self.warn_about_default_passwords()

@classmethod
Expand Down
6 changes: 3 additions & 3 deletions azafea_metrics_proxy/config/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@

def is_boolean(value: Any) -> bool:
if not isinstance(value, bool):
raise TypeError(f'{value!r} is not a boolean')
raise ValueError(f'{value!r} is not a boolean')

return value


def is_non_empty_string(value: Any) -> str:
if not isinstance(value, str):
raise TypeError(f'{value!r} is not a string')
raise ValueError(f'{value!r} is not a string')

if not value:
raise ValueError(f'{value!r} is empty')
Expand All @@ -42,7 +42,7 @@ def is_non_empty_string(value: Any) -> str:

def is_strictly_positive_integer(value: Any) -> int:
if type(value) is not int:
raise TypeError(f'{value!r} is not an integer')
raise ValueError(f'{value!r} is not an integer')

if value < 1:
raise ValueError(f'{value!r} is not a strictly positive integer')
Expand Down
5 changes: 0 additions & 5 deletions azafea_metrics_proxy/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@

def pytest_collection_modifyitems(items):
for item in items:
markers = [m for m in item.own_markers if m.name in ('flake8', 'mypy')]

if markers:
continue

if item.nodeid.startswith('azafea_metrics_proxy/tests/integration/'):
item.add_marker(pytest.mark.integration)

Expand Down
4 changes: 1 addition & 3 deletions azafea_metrics_proxy/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
# along with azafea-metrics-proxy. If not, see <http://www.gnu.org/licenses/>.


from aioredis import Redis

import pytest

from azafea_metrics_proxy.app import get_app
Expand All @@ -28,7 +26,7 @@ async def app(make_config):
config = make_config({})

app = await get_app(config)
redis: Redis = app['redis']
redis = app['redis']

async def clear_queues():
queues = await redis.keys('*')
Expand Down
10 changes: 8 additions & 2 deletions azafea_metrics_proxy/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ def test_print_invalid_config(capfd, make_config_file):
azafea_metrics_proxy.cli.run_command('-c', str(config_file), 'print-config')

capture = capfd.readouterr()
assert "Invalid configuration:\n* main.verbose: 'blah' is not a boolean" in capture.err
assert (
"Invalid configuration:\n"
"* main.verbose: Value error, 'blah' is not a boolean"
) in capture.err


def test_run(capfd, monkeypatch, make_config_file):
Expand Down Expand Up @@ -79,7 +82,10 @@ def test_run_invalid_config(capfd, make_config_file):
azafea_metrics_proxy.cli.run_command('-c', str(config_file), 'run')

capture = capfd.readouterr()
assert "Invalid configuration:\n* main.verbose: 'blah' is not a boolean" in capture.err
assert (
"Invalid configuration:\n"
"* main.verbose: Value error, 'blah' is not a boolean"
) in capture.err


def test_run_redis_invalid_host(capfd, make_config_file):
Expand Down
30 changes: 20 additions & 10 deletions azafea_metrics_proxy/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ def test_override_verbose_invalid(make_config, value):
with pytest.raises(azafea_metrics_proxy.config.InvalidConfigurationError) as exc_info:
make_config({'main': {'verbose': value}})

assert ('Invalid configuration:\n'
f'* main.verbose: {value!r} is not a boolean') in str(exc_info.value)
assert (
'Invalid configuration:\n'
f'* main.verbose: Value error, {value!r} is not a boolean'
) in str(exc_info.value)


@pytest.mark.parametrize('value', [
Expand All @@ -100,16 +102,20 @@ def test_override_redis_host_invalid(make_config, value):
with pytest.raises(azafea_metrics_proxy.config.InvalidConfigurationError) as exc_info:
make_config({'redis': {'host': value}})

assert ('Invalid configuration:\n'
f'* redis.host: {value!r} is not a string') in str(exc_info.value)
assert (
'Invalid configuration:\n'
f'* redis.host: Value error, {value!r} is not a string'
) in str(exc_info.value)


def test_override_redis_host_empty(make_config):
with pytest.raises(azafea_metrics_proxy.config.InvalidConfigurationError) as exc_info:
make_config({'redis': {'host': ''}})

assert ('Invalid configuration:\n'
"* redis.host: '' is empty") in str(exc_info.value)
assert (
'Invalid configuration:\n'
"* redis.host: Value error, '' is empty"
) in str(exc_info.value)


@pytest.mark.parametrize('value', [
Expand All @@ -121,8 +127,10 @@ def test_override_redis_port_invalid(make_config, value):
with pytest.raises(azafea_metrics_proxy.config.InvalidConfigurationError) as exc_info:
make_config({'redis': {'port': value}})

assert ('Invalid configuration:\n'
f'* redis.port: {value!r} is not an integer') in str(exc_info.value)
assert (
'Invalid configuration:\n'
f'* redis.port: Value error, {value!r} is not an integer'
) in str(exc_info.value)


@pytest.mark.parametrize('value', [
Expand All @@ -133,8 +141,10 @@ def test_override_redis_port_not_positive(make_config, value):
with pytest.raises(azafea_metrics_proxy.config.InvalidConfigurationError) as exc_info:
make_config({'redis': {'port': value}})

assert ('Invalid configuration:\n'
f'* redis.port: {value!r} is not a strictly positive integer') in str(exc_info.value)
assert (
'Invalid configuration:\n'
f'* redis.port: Value error, {value!r} is not a strictly positive integer'
) in str(exc_info.value)


def test_default_passwords(capfd):
Expand Down
4 changes: 3 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ license_file = COPYING

[options]
packages = find:
python_requires = >= 3.7
python_requires = >= 3.11

[options.package_data]
azafea_metrics_proxy = py.typed
Expand All @@ -40,6 +40,7 @@ max-line-length = 100

[mypy]
disallow_untyped_defs = True
files = azafea_metrics_proxy
mypy_path = stubs
plugins = pydantic.mypy
warn_redundant_casts = True
Expand All @@ -49,6 +50,7 @@ warn_unused_ignores = True
disallow_untyped_defs = False

[tool:pytest]
asyncio_mode = auto
filterwarnings =
error::DeprecationWarning:azafea_metrics_proxy.*
ignore::DeprecationWarning:distutils.*
Expand Down
2 changes: 1 addition & 1 deletion stubs/freezegun.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class _freeze_time: ...

def freeze_time(time_to_freeze: Optional[Union[str, date, timedelta, Callable, Generator]] = None,
tz_offset: Union[int, timedelta] = 0,
ignore: List = None,
ignore: Optional[List] = None,
tick: bool = False,
as_arg: bool = False,
auto_tick_seconds: int = 0) -> _freeze_time: ...
10 changes: 5 additions & 5 deletions stubs/pytest.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
# along with azafea-metrics-proxy. If not, see <http://www.gnu.org/licenses/>.


from typing import Any, Callable, List
from typing import Any, Callable, List, Optional


def fixture(scope: str = "function",
params: List[Any] = None,
params: Optional[List[Any]] = None,
autouse: bool = False,
ids: List[str] = None,
name: str = None) -> Callable: ...
ids: Optional[List[str]] = None,
name: Optional[str] = None) -> Callable: ...

class mark:
@staticmethod
def parametrize(names: str, params: List[Any]) -> Callable: ...

def param(*values: Any, id: str = None) -> Callable: ...
def param(*values: Any, id: Optional[str] = None) -> Callable: ...

0 comments on commit b8803b4

Please sign in to comment.