Source code for language_tool_python._internals.utils
import contextlib
import locale
import logging
import math
import os
import urllib.parse
from pathlib import Path
from typing import Protocol, runtime_checkable
import psutil
from language_tool_python.exceptions import PathError
__all__ = [
"SupportsBool",
"get_env_float",
"get_env_int",
"get_language_tool_download_path",
"get_locale_language",
"kill_process_force",
"parse_url",
"version_tuple",
]
logger = logging.getLogger(__name__)
FAILSAFE_LANGUAGE = "en"
LTP_PATH_ENV_VAR = "LTP_PATH" # LanguageTool download path
def parse_url(url_str: str) -> str:
"""Parse the given URL string and ensure it has a scheme.
If the input URL string does not contain 'http', 'http://' is prepended to it. The
function then parses the URL and returns its canonical form.
:param url_str: The URL string to be parsed.
:type url_str: str
:return: The parsed URL in its canonical form.
:rtype: str
"""
if "http" not in url_str:
url_str = "http://" + url_str
return urllib.parse.urlparse(url_str).geturl()
def get_env_int(env_var: str, default: int) -> int:
"""Read a positive integer from the environment.
:param env_var: Environment variable name.
:type env_var: str
:param default: Value to use when the environment variable is absent.
:type default: int
:return: Configured integer value, or the default.
:rtype: int
:raises PathError: If the configured value is invalid.
"""
configured = os.environ.get(env_var)
if configured is None:
return default
try:
value = int(configured)
except ValueError as e:
err = f"Invalid integer configured by {env_var}: {configured!r}."
raise PathError(err) from e
if value <= 0:
err = f"Invalid integer configured by {env_var}: {configured!r}."
raise PathError(err)
return value
def get_env_float(env_var: str, default: float) -> float:
"""Read a positive float from the environment.
:param env_var: Environment variable name.
:type env_var: str
:param default: Value to use when the environment variable is absent.
:type default: float
:return: Configured float value, or the default.
:rtype: float
:raises PathError: If the configured value is invalid.
"""
configured = os.environ.get(env_var)
if configured is None:
return default
try:
value = float(configured)
except ValueError as e:
err = f"Invalid float configured by {env_var}: {configured!r}."
raise PathError(err) from e
if not math.isfinite(value) or value <= 0:
err = f"Invalid float configured by {env_var}: {configured!r}."
raise PathError(err)
return value
def get_language_tool_download_path() -> Path:
"""Get the download path for LanguageTool.
This function retrieves the download path for LanguageTool from the environment
variable specified by ``LTP_PATH_ENV_VAR``. If the environment variable is not set,
it defaults to a path in the user's home directory under
``.cache/language_tool_python``. The function ensures that the directory exists
before returning it.
:return: The download path for LanguageTool.
:rtype: Path
"""
# Get download path from environment or use default.
path_str = os.environ.get(
LTP_PATH_ENV_VAR,
str(Path.home() / ".cache" / "language_tool_python"),
)
path = Path(path_str)
path.mkdir(parents=True, exist_ok=True)
return path
def get_locale_language() -> str:
"""Get the current locale language.
This function retrieves the current locale language setting of the system. It first
attempts to get the locale using ``locale.getlocale()``. If that fails, it falls
back to using ``locale.getdefaultlocale()``. If both methods fail to provide a valid
language code, it returns a default failsafe language code.
:return: The language code of the current locale.
:rtype: str
"""
return locale.getlocale()[0] or locale.getdefaultlocale()[0] or FAILSAFE_LANGUAGE
def kill_process_force(
*,
pid: int | None = None,
proc: psutil.Process | None = None,
) -> None:
"""Terminate a process and all of its child processes forcefully.
This function attempts to kill a process specified either by its PID or by a
psutil.Process object. If the process has any child processes, they will be killed
first.
:param pid: The process ID of the process to be killed. Either ``pid`` or ``proc``
must be provided.
:type pid: int | None
:param proc: A psutil.Process object representing the process to be killed. Either
``pid`` or ``proc`` must be provided.
:type proc: psutil.Process | None
:raises ValueError: If neither ``pid`` nor ``proc`` is provided.
"""
if not any([pid, proc]):
err = "Must pass either pid or proc"
raise ValueError(err)
try:
proc = psutil.Process(pid) if proc is None else proc
except psutil.NoSuchProcess:
logger.debug("Process %s does not exist, nothing to kill", pid)
return
logger.debug("Killing process %s and its children", proc.pid)
for child in proc.children(recursive=True):
with contextlib.suppress(psutil.NoSuchProcess):
logger.debug("Killing child process %s", child.pid)
child.kill()
with contextlib.suppress(psutil.NoSuchProcess):
proc.kill()
[docs]
@runtime_checkable
class SupportsBool(Protocol):
"""Protocol for types that can be converted to a boolean value."""
def __bool__(self) -> bool:
"""Define the interface for types that can be evaluated in a boolean context."""
def version_tuple(v: str) -> tuple[int, int]:
"""Convert a version string into a tuple of integers.
This function takes a version string in the format 'X.Y' and converts it into a
tuple of integers (X, Y). This can be useful for comparing version numbers.
:param v: The version string to be converted, expected in the format 'X.Y'.
:type v: str
:return: A tuple of integers representing the version, in the format (X, Y).
:rtype: tuple[int, int]
:raises ValueError: If the version string is not in the expected format or contains
non-integer components.
"""
major, minor = v.split(".")
return int(major), int(minor)