Skip to content

Commit

Permalink
Support Python 3.13, drop support for Python 3.8
Browse files Browse the repository at this point in the history
  • Loading branch information
fizyk committed Oct 11, 2024
1 parent a6935de commit d58854d
Show file tree
Hide file tree
Showing 17 changed files with 50 additions and 90 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ jobs:
uses: actions/checkout@v4
- uses: fizyk/actions-reuse/.github/actions/[email protected]
with:
python-version: "3.12"
python-version: "3.13"
command: tbump --dry-run --only-patch $(pipenv run tbump current-version)"-x"
towncrier:
runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- uses: fizyk/actions-reuse/.github/actions/[email protected]
with:
python-version: "3.12"
python-version: "3.13"
command: towncrier check --compare-with origin/main
fetch-depth: 0
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ jobs:
tests:
uses: fizyk/actions-reuse/.github/workflows/[email protected]
with:
python-versions: '["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.10"]'
python-versions: '["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.10"]'
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
macostests:
uses: fizyk/actions-reuse/.github/workflows/[email protected]
needs: [tests]
with:
python-versions: '["3.10", "3.11", "3.12", "pypy-3.10"]'
python-versions: '["3.11", "3.12", "3.13", "pypy-3.10"]'
os: macos-latest
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
9 changes: 2 additions & 7 deletions mirakuru/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,7 @@ def err_output(self) -> Optional[IO[Any]]:
return self.process.stderr
return None # pragma: no cover

def wait_for(
self: SimpleExecutorType, wait_for: Callable[[], bool]
) -> SimpleExecutorType:
def wait_for(self: SimpleExecutorType, wait_for: Callable[[], bool]) -> SimpleExecutorType:
"""Wait for callback to return True.
Simply returns if wait_for condition has been met,
Expand Down Expand Up @@ -459,10 +457,7 @@ def __del__(self) -> None:
self.kill()
except Exception: # pragma: no cover
print("*" * 80)
print(
"Exception while deleting Executor. '"
"It is strongly suggested that you use"
)
print("Exception while deleting Executor. It is strongly suggested that you use")
print("it as a context manager instead.")
print("*" * 80)
raise
Expand Down
8 changes: 2 additions & 6 deletions mirakuru/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ def __init__(self, executor: "SimpleExecutor") -> None:
class TimeoutExpired(ExecutorError):
"""Is raised when the timeout expires while starting an executor."""

def __init__(
self, executor: "SimpleExecutor", timeout: Union[int, float]
) -> None:
def __init__(self, executor: "SimpleExecutor", timeout: Union[int, float]) -> None:
"""Exception initialization with an extra ``timeout`` argument.
:param mirakuru.base.SimpleExecutor executor: for which exception
Expand All @@ -40,9 +38,7 @@ def __str__(self) -> str:
:returns: string representation
:rtype: str
"""
return (
f"Executor {self.executor} timed out after {self.timeout} seconds"
)
return f"Executor {self.executor} timed out after {self.timeout} seconds"


class AlreadyRunning(ExecutorError):
Expand Down
4 changes: 1 addition & 3 deletions mirakuru/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,5 @@ def after_start_check(self) -> bool:
return False

except (HTTPException, socket.timeout, socket.error) as ex:
LOG.debug(
"Encounter %s while trying to check if service has started.", ex
)
LOG.debug("Encounter %s while trying to check if service has started.", ex)
return False
9 changes: 2 additions & 7 deletions mirakuru/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ def __init__(
super().__init__(command, **kwargs)
self._banner = re.compile(banner)
if not any((self._stdout, self._stderr)):
raise TypeError(
"At least one of stdout or stderr has to be initialized"
)
raise TypeError("At least one of stdout or stderr has to be initialized")

def start(self: OutputExecutorType) -> OutputExecutorType:
"""Start process.
Expand All @@ -85,10 +83,7 @@ def start(self: OutputExecutorType) -> OutputExecutorType:

output_file = output_method()
if output_file is None:
raise ValueError(
"The process is started but "
"the output file is None"
)
raise ValueError("The process is started but the output file is None")
# register a file descriptor
# POLLIN because we will wait for data to read
std_poll.register(output_file, select.POLLIN)
Expand Down
1 change: 1 addition & 0 deletions newsfragments/+0e5f4193.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for Python 3.13
3 changes: 3 additions & 0 deletions newsfragments/+ceb0e424.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Extended line-lenght to 100 characters
* updated test_forgotten_stop as on CI on
Python 3.13 it lost one character out of the marker
1 change: 1 addition & 0 deletions newsfragments/+f5d4730a.break.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dropped support for Python 3.8 (As it reached EOL)
12 changes: 7 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules",
]
Expand All @@ -30,7 +30,7 @@ dependencies = [
# <https://github.com/giampaolo/psutil/issues/82>.
"psutil>=4.0.0; sys_platform != 'cygwin'",
]
requires-python = ">= 3.8"
requires-python = ">= 3.9"

[project.urls]
"Source" = "https://github.com/ClearcodeHQ/mirakuru"
Expand Down Expand Up @@ -85,12 +85,14 @@ filterwarnings = "error"
xfail_strict = "True"

[tool.black]
line-length = 80
target-version = ['py38']
line-length = 100
target-version = ['py39']
include = '.*\.pyi?$'

[tool.ruff]
line-length = 80
line-length = 100

[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
Expand Down
7 changes: 2 additions & 5 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,5 @@


def ps_aux() -> str:
"""Return output of systems `ps aux -w` call.
:rtype str
"""
return str(check_output(("ps", "aux", "-w")))
"""Return output of systems `ps aux -w` call."""
return check_output(("ps", "aux", "-w")).decode()
35 changes: 12 additions & 23 deletions tests/executors/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,13 @@ def test_stop_custom_exit_signal_stop() -> None:
executor = SimpleExecutor("false", shell=True)
executor.start()
# false exits instant, so there should not be a process to stop
retry(
lambda: executor.stop(
stop_signal=signal.SIGQUIT, expected_returncode=-3
)
)
retry(lambda: executor.stop(stop_signal=signal.SIGQUIT, expected_returncode=-3))
assert executor.running() is False


def test_stop_custom_exit_signal_context() -> None:
"""Start process and expect custom exit signal in context manager."""
with SimpleExecutor(
"false", expected_returncode=-3, shell=True
) as executor:
with SimpleExecutor("false", expected_returncode=-3, shell=True) as executor:
executor.stop(stop_signal=signal.SIGQUIT)
assert executor.running() is False

Expand Down Expand Up @@ -160,24 +154,25 @@ def test_forgotten_stop() -> None:
If someone forgot to stop() or kill() subprocess it should be killed
by default on instance cleanup.
"""
mark = str(uuid.uuid1())
mark = uuid.uuid1().hex
# We cannot simply do `sleep 300 #<our-uuid>` in a shell because in that
# case bash (default shell on some systems) does `execve` without cloning
# itself - that means there will be no process with commandline like:
# '/bin/sh -c sleep 300 && true #<our-uuid>' - instead that process would
# get substituted with 'sleep 300' and the marked commandline would be
# overwritten.
# Injecting some flow control (`&&`) forces bash to fork properly.
marked_command = f"sleep 300 && true #{mark!s}"
marked_command = f"sleep 300 && true #{mark}"
executor = SimpleExecutor(marked_command, shell=True)
executor.start()
assert executor.running() is True
assert mark in ps_aux(), "The test process should be running."
ps_output = ps_aux()
assert (
mark in ps_output
), f"The test command {marked_command} should be running in \n\n {ps_output}."
del executor
gc.collect() # to force 'del' immediate effect
assert (
mark not in ps_aux()
), "The test process should not be running at this point."
assert mark not in ps_aux(), "The test process should not be running at this point."


def test_executor_raises_if_process_exits_with_error() -> None:
Expand All @@ -187,16 +182,10 @@ def test_executor_raises_if_process_exits_with_error() -> None:
should raise an exception.
"""
error_code = 12
failing_executor = Executor(
["bash", "-c", f"exit {error_code!s}"], timeout=5
)
failing_executor.pre_start_check = mock.Mock( # type: ignore
return_value=False
)
failing_executor = Executor(["bash", "-c", f"exit {error_code!s}"], timeout=5)
failing_executor.pre_start_check = mock.Mock(return_value=False) # type: ignore
# After-start check will keep returning False to let the process terminate.
failing_executor.after_start_check = mock.Mock( # type: ignore
return_value=False
)
failing_executor.after_start_check = mock.Mock(return_value=False) # type: ignore

with pytest.raises(ProcessExitedWithError) as exc:
failing_executor.start()
Expand Down
7 changes: 4 additions & 3 deletions tests/executors/test_executor_kill.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ def raise_os_error(*_: int, **__: int) -> NoReturn:
def processes_with_env_mock(*_: str, **__: str) -> Set[int]:
return {1}

with patch(
"mirakuru.base.processes_with_env", new=processes_with_env_mock
), patch("os.kill", new=raise_os_error):
with (
patch("mirakuru.base.processes_with_env", new=processes_with_env_mock),
patch("os.kill", new=raise_os_error),
):
executor = SimpleExecutor(SLEEP_300)
executor._kill_all_kids(executor._stop_signal)
20 changes: 5 additions & 15 deletions tests/executors/test_http_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ def test_executor_starts_and_waits() -> None:

def test_shell_started_server_stops() -> None:
"""Test if executor terminates properly executor with shell=True."""
executor = HTTPExecutor(
HTTP_NORMAL_CMD, f"http://{HOST}:{PORT}/", timeout=20, shell=True
)
executor = HTTPExecutor(HTTP_NORMAL_CMD, f"http://{HOST}:{PORT}/", timeout=20, shell=True)

with pytest.raises(socket.error):
connect_to_server()
Expand All @@ -77,9 +75,7 @@ def test_slow_method_server_starting(method: str) -> None:
Simple example. You run Gunicorn and it is working but you have to
wait for worker processes.
"""
http_method_slow_cmd = (
f"{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False {method}"
)
http_method_slow_cmd = f"{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False {method}"
with HTTPExecutor(
http_method_slow_cmd,
f"http://{HOST}:{PORT}/",
Expand All @@ -96,9 +92,7 @@ def test_slow_post_payload_server_starting() -> None:
Simple example. You run Gunicorn and it is working but you have to
wait for worker processes.
"""
http_method_slow_cmd = (
f"{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False Key"
)
http_method_slow_cmd = f"{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False Key"
with HTTPExecutor(
http_method_slow_cmd,
f"http://{HOST}:{PORT}/",
Expand All @@ -113,9 +107,7 @@ def test_slow_post_payload_server_starting() -> None:
@pytest.mark.parametrize("method", ("HEAD", "GET", "POST"))
def test_slow_method_server_timed_out(method: str) -> None:
"""Check if timeout properly expires."""
http_method_slow_cmd = (
f"{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False {method}"
)
http_method_slow_cmd = f"{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False {method}"
executor = HTTPExecutor(
http_method_slow_cmd, f"http://{HOST}:{PORT}/", method=method, timeout=1
)
Expand Down Expand Up @@ -183,9 +175,7 @@ def test_default_port() -> None:
("(200|404)", False),
),
)
def test_http_status_codes(
accepted_status: Union[None, int, str], expected_timeout: bool
) -> None:
def test_http_status_codes(accepted_status: Union[None, int, str], expected_timeout: bool) -> None:
"""Test how 'status' argument influences executor start.
:param int|str accepted_status: Executor 'status' value
Expand Down
8 changes: 2 additions & 6 deletions tests/executors/test_unixsocket_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@

def test_start_and_wait() -> None:
"""Test if executor await for process to accept connections."""
executor = UnixSocketExecutor(
SOCKET_SERVER_CMD + " 2", socket_name=SOCKET_PATH, timeout=5
)
executor = UnixSocketExecutor(SOCKET_SERVER_CMD + " 2", socket_name=SOCKET_PATH, timeout=5)
with executor:
assert executor.running() is True


def test_start_and_timeout() -> None:
"""Test if executor will properly times out."""
executor = UnixSocketExecutor(
SOCKET_SERVER_CMD + " 10", socket_name=SOCKET_PATH, timeout=5
)
executor = UnixSocketExecutor(SOCKET_SERVER_CMD + " 10", socket_name=SOCKET_PATH, timeout=5)

with pytest.raises(TimeoutExpired):
executor.start()
Expand Down
4 changes: 1 addition & 3 deletions tests/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,5 @@ def retry(
return res
except possible_exception as e:
if time + timeout_diff < datetime.now(timezone.utc):
raise TimeoutError(
"Failed after {i} attempts".format(i=i)
) from e
raise TimeoutError("Failed after {i} attempts".format(i=i)) from e
sleep(1)
4 changes: 1 addition & 3 deletions tests/server_for_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ def do_HEAD(self) -> None: # pylint:disable=invalid-name
if ast.literal_eval(IMMORTAL):
block_signals()

server = HTTPServer(
(HOST, int(PORT)), HANDLERS[METHOD]
) # pylint: disable=invalid-name
server = HTTPServer((HOST, int(PORT)), HANDLERS[METHOD]) # pylint: disable=invalid-name
print(f"Starting slow server on {HOST}:{PORT}...")
server.serve_forever()

0 comments on commit d58854d

Please sign in to comment.