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

Support Python 3.13, drop support for Python 3.8 #798

Merged
merged 1 commit into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Loading