Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Python 3.12 in CI and as the main target #1067

Merged
merged 9 commits into from
Jan 19, 2024
8 changes: 4 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version: "3.12"
- run: pip install -r requirements.txt
- run: pre-commit run --all-files
- run: mypy kopf --strict
Expand All @@ -38,10 +38,10 @@ jobs:
fail-fast: false
matrix:
install-extras: [ "", "full-auth" ]
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
include:
- install-extras: "uvloop"
python-version: "3.11"
python-version: "3.12"
name: Python ${{ matrix.python-version }} ${{ matrix.install-extras }}
runs-on: ubuntu-22.04
timeout-minutes: 5 # usually 2-3 mins
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version: "3.12"
- uses: nolar/setup-k3d-k3s@v1
with:
version: ${{ matrix.k3s }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version: "3.12"
- run: pip install --upgrade setuptools wheel twine
- run: python setup.py sdist bdist_wheel
- uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/thorough.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version: "3.12"
- run: pip install -r requirements.txt
- run: pre-commit run --all-files
- run: mypy kopf --strict
Expand All @@ -42,10 +42,10 @@ jobs:
fail-fast: false
matrix:
install-extras: [ "", "full-auth" ]
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
include:
- install-extras: "uvloop"
python-version: "3.11"
python-version: "3.12"
name: Python ${{ matrix.python-version }} ${{ matrix.install-extras }}
runs-on: ubuntu-22.04
timeout-minutes: 5 # usually 2-3 mins
Expand Down Expand Up @@ -115,7 +115,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version: "3.12"
- uses: nolar/setup-k3d-k3s@v1
with:
version: ${{ matrix.k3s }}
Expand All @@ -137,7 +137,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version: "3.12"
- run: tools/install-minikube.sh
- run: pip install -r requirements.txt -r examples/requirements.txt
- run: pytest --color=yes --timeout=30 --only-e2e
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ We assume that when the operator is executed in the cluster, it must be packaged
into a docker image with a CI/CD tool of your preference.

```dockerfile
FROM python:3.11
FROM python:3.12
ADD . /src
RUN pip install kopf
CMD kopf run /src/handlers.py --verbose
Expand Down
2 changes: 1 addition & 1 deletion docs/deployment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ First of all, the operator must be packaged as a docker image with Python 3.8 or
:caption: Dockerfile
:name: dockerfile

FROM python:3.11
FROM python:3.12
RUN pip install kopf
ADD . /src
CMD kopf run /src/handlers.py --verbose
Expand Down
5 changes: 2 additions & 3 deletions kopf/_cogs/helpers/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@
version: Optional[str] = None

try:
import pkg_resources
import importlib.metadata
except ImportError:
pass
else:
try:
name, *_ = __name__.split('.') # usually "kopf", unless renamed/forked.
dist: pkg_resources.Distribution = pkg_resources.get_distribution(name)
version = dist.version
version = importlib.metadata.version(name)
except Exception:
pass # installed as an egg, from git, etc.
2 changes: 1 addition & 1 deletion kopf/_core/actions/loggers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class LogFormat(enum.Enum):
""" Log formats, as specified on CLI. """
PLAIN = '%(message)s'
FULL = '[%(asctime)s] %(name)-20.20s [%(levelname)-8.8s] %(message)s'
JSON = enum.auto()
JSON = '-json-' # not used for formatting, only for detection


class ObjectFormatter(logging.Formatter):
Expand Down
9 changes: 5 additions & 4 deletions kopf/_core/engines/posting.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,14 @@ def createLock(self) -> None:
def filter(self, record: logging.LogRecord) -> bool:
# Only those which have a k8s object referred (see: `ObjectLogger`).
# Otherwise, we have nothing to post, and nothing to do.
# TODO: remove all bool() -- they were needed for Python 3.12 & MyPy 1.8.0 wrong inference.
settings: Optional[configuration.OperatorSettings]
settings = getattr(record, 'settings', None)
level_ok = settings is not None and record.levelno >= settings.posting.level
enabled = settings is not None and settings.posting.enabled
level_ok = settings is not None and bool(record.levelno >= settings.posting.level)
enabled = settings is not None and bool(settings.posting.enabled)
has_ref = hasattr(record, 'k8s_ref')
skipped = hasattr(record, 'k8s_skip') and getattr(record, 'k8s_skip')
return enabled and level_ok and has_ref and not skipped and super().filter(record)
skipped = hasattr(record, 'k8s_skip') and bool(getattr(record, 'k8s_skip'))
return enabled and level_ok and has_ref and not skipped and bool(super().filter(record))

def emit(self, record: logging.LogRecord) -> None:
# Same try-except as in e.g. `logging.StreamHandler`.
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pre-commit
pyngrok
pytest>=6.0.0
pytest-aiohttp
pytest-asyncio<0.22 # until the "event_loop" deprecation is solved
pytest-asyncio
pytest-cov
pytest-mock
pytest-timeout
Expand Down
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
Expand All @@ -61,7 +62,8 @@
'python-json-logger', # 0.05 MB
'iso8601', # 0.07 MB
'click', # 0.60 MB
'aiohttp<4.0.0', # 7.80 MB
'aiohttp', # 7.80 MB
'aiohttp>=3.9.0; python_version>="3.12"',
'pyyaml', # 0.90 MB
],
extras_require={
Expand All @@ -71,6 +73,7 @@
],
'uvloop': [
'uvloop', # 9.00 MB
'uvloop>=0.18.0; python_version>="3.12"',
],
'dev': [
'pyngrok', # 1.00 MB + downloaded binary
Expand Down
19 changes: 16 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ def pytest_configure(config):
# TODO: Remove when fixed in https://github.com/pytest-dev/pytest-asyncio/issues/460:
config.addinivalue_line('filterwarnings', 'ignore:There is no current event loop:DeprecationWarning:pytest_asyncio')

# Python 3.12 transitional period:
config.addinivalue_line('filterwarnings', 'ignore:datetime*:DeprecationWarning:dateutil')
config.addinivalue_line('filterwarnings', 'ignore:datetime*:DeprecationWarning:freezegun')
config.addinivalue_line('filterwarnings', 'ignore:.*:DeprecationWarning:_pydevd_.*')


def pytest_addoption(parser):
parser.addoption("--only-e2e", action="store_true", help="Execute end-to-end tests only.")
Expand Down Expand Up @@ -698,8 +703,13 @@ def assert_logs_fn(patterns, prohibited=[], strict=False):
#
# Helpers for asyncio checks.
#
@pytest.fixture()
async def loop():
yield asyncio.get_running_loop()


@pytest.fixture(autouse=True)
def _no_asyncio_pending_tasks(event_loop):
def _no_asyncio_pending_tasks(loop: asyncio.AbstractEventLoop):
"""
Ensure there are no unattended asyncio tasks after the test.

Expand All @@ -720,7 +730,7 @@ def _no_asyncio_pending_tasks(event_loop):

# Let the pytest-asyncio's async2sync wrapper to finish all callbacks. Otherwise, it raises:
# <Task pending name='Task-2' coro=<<async_generator_athrow without __name__>()>>
event_loop.run_until_complete(asyncio.sleep(0))
loop.run_until_complete(asyncio.sleep(0))

# Detect all leftover tasks.
after = _get_all_tasks()
Expand All @@ -734,7 +744,10 @@ def _get_all_tasks() -> Set[asyncio.Task]:
i = 0
while True:
try:
tasks = list(asyncio.tasks._all_tasks)
if sys.version_info >= (3, 12):
tasks = asyncio.tasks._eager_tasks | set(asyncio.tasks._scheduled_tasks)
else:
tasks = list(asyncio.tasks._all_tasks)
except RuntimeError:
i += 1
if i >= 1000:
Expand Down
6 changes: 3 additions & 3 deletions tests/logging/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ def _caplog_all_levels(caplog):


@pytest.fixture(autouse=True)
def event_queue_loop(event_loop):
token = event_queue_loop_var.set(event_loop)
def event_queue_loop(loop): # must be sync-def
token = event_queue_loop_var.set(loop)
try:
yield event_loop
yield loop
finally:
event_queue_loop_var.reset(token)

Expand Down
6 changes: 3 additions & 3 deletions tests/posting/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@


@pytest.fixture()
def event_queue_loop(event_loop):
token = event_queue_loop_var.set(event_loop)
def event_queue_loop(loop): # must be sync-def
token = event_queue_loop_var.set(loop)
try:
yield event_loop
yield loop
finally:
event_queue_loop_var.reset(token)

Expand Down
4 changes: 2 additions & 2 deletions tests/posting/test_threadsafety.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@


@pytest.fixture()
def awakener(event_loop):
def awakener():
handles = []

def noop():
pass

def awaken_fn(delay, fn=noop):
handle = event_loop.call_later(delay, fn)
handle = asyncio.get_running_loop().call_later(delay, fn)
handles.append(handle)

try:
Expand Down
5 changes: 3 additions & 2 deletions tests/primitives/test_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async def test_no_triggering():
await asyncio.wait([task])


async def test_triggering(event_loop, timer):
async def test_triggering(timer):
source = asyncio.Condition()
target = asyncio.Condition()
task = asyncio.create_task(condition_chain(source, target))
Expand All @@ -28,7 +28,8 @@ async def delayed_trigger():
async with source:
source.notify_all()

event_loop.call_later(0.1, asyncio.create_task, delayed_trigger())
loop = asyncio.get_running_loop()
loop.call_later(0.1, asyncio.create_task, delayed_trigger())

with timer:
async with target:
Expand Down
21 changes: 12 additions & 9 deletions tests/primitives/test_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ async def test_empty_by_default():
await asyncio.wait_for(container.wait(), timeout=0.1)


async def test_does_not_wake_up_when_reset(event_loop, timer):
async def test_does_not_wake_up_when_reset(timer):
container = Container()

async def reset_it():
await container.reset()

event_loop.call_later(0.05, asyncio.create_task, reset_it())
loop = asyncio.get_running_loop()
loop.call_later(0.05, asyncio.create_task, reset_it())

with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(container.wait(), timeout=0.1)


async def test_wakes_up_when_preset(event_loop, timer):
async def test_wakes_up_when_preset(timer):
container = Container()
await container.set(123)

Expand All @@ -34,13 +35,14 @@ async def test_wakes_up_when_preset(event_loop, timer):
assert result == 123


async def test_wakes_up_when_set(event_loop, timer):
async def test_wakes_up_when_set(timer):
container = Container()

async def set_it():
await container.set(123)

event_loop.call_later(0.1, asyncio.create_task, set_it())
loop = asyncio.get_running_loop()
loop.call_later(0.1, asyncio.create_task, set_it())

with timer:
result = await container.wait()
Expand All @@ -49,14 +51,15 @@ async def set_it():
assert result == 123


async def test_iterates_when_set(event_loop, timer):
async def test_iterates_when_set(timer):
container = Container()

async def set_it(v):
await container.set(v)

event_loop.call_later(0.1, asyncio.create_task, set_it(123))
event_loop.call_later(0.2, asyncio.create_task, set_it(234))
loop = asyncio.get_running_loop()
loop.call_later(0.1, asyncio.create_task, set_it(123))
loop.call_later(0.2, asyncio.create_task, set_it(234))

values = []
with timer:
Expand All @@ -69,7 +72,7 @@ async def set_it(v):
assert values == [123, 234]


async def test_iterates_when_preset(event_loop, timer):
async def test_iterates_when_preset(timer):
container = Container()
await container.set(123)

Expand Down
6 changes: 3 additions & 3 deletions tests/reactor/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def watcher_limited(mocker, settings):


@pytest.fixture()
def watcher_in_background(settings, resource, event_loop, worker_spy, stream):
async def watcher_in_background(settings, resource, worker_spy, stream):

# Prevent remembering the streaming objects in the mocks.
async def do_nothing(*args, **kwargs):
Expand All @@ -57,7 +57,7 @@ async def do_nothing(*args, **kwargs):
settings=settings,
processor=do_nothing,
)
task = event_loop.create_task(coro)
task = asyncio.create_task(coro)

try:
# Go for a test.
Expand All @@ -66,6 +66,6 @@ async def do_nothing(*args, **kwargs):
# Terminate the watcher to cleanup the loop.
task.cancel()
try:
event_loop.run_until_complete(task)
await task
except asyncio.CancelledError:
pass # cancellations are expected at this point
2 changes: 1 addition & 1 deletion tests/reactor/test_queueing.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async def test_watchevent_demultiplexing(worker_mock, timer, resource, processor
])
@pytest.mark.usefixtures('watcher_limited')
async def test_watchevent_batching(settings, resource, processor, timer,
stream, events, uids, vals, event_loop):
stream, events, uids, vals):
""" Verify that only the last event per uid is actually handled. """

# Override the default timeouts to make the tests faster.
Expand Down
Loading