Skip to content

Commit

Permalink
Add mypy as static type checker (#129)
Browse files Browse the repository at this point in the history
* umu_consts: add XDG_DATA_HOME

* umu_consts: fix type

* umu_plugins: fix type

* umu_plugins: fix types

* umu_log: fix type

* umu_util: fix types

* umu_runtime: fix types

* umu_proton: fix types

* umu_run: fix types

* Add pyproject.toml

- Configuration file for mypy

* workflows: add mypy workflow

* Revert "umu_consts: add XDG_DATA_HOME"

This reverts commit c5ceb71.

* umu_consts: update FLATPAK_PATH

- Flatpak guarantees the existence of XDG_DATA_HOME and other XDG environment variables

* Ruff lint

* workflows: update static.yml

* umu_log: fix type

* Update pyproject.toml

* umu_run: update format

* Fix module imports

- Since we've already imported the os and sys modules in our entry point, no need to selectively import functions from them

* umu_util: update format

* umu_proton: don't initialize vars

* umu_run: add fixme tag to runtime workaround

* umu_runtime: move uninitialized var to top level scope

* umu_runtime: remove return statement

* umu_runtime: update string slicing logic

* umu_runtime: fix string logic
  • Loading branch information
R1kaB3rN authored Jun 29, 2024
1 parent 20c1d09 commit de17548
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 126 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: mypy

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read

jobs:
build:
strategy:
matrix:
version: ["3.10"]

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.version }}
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
- name: Check types with mypy
run: |
pip install mypy
cd umu && mypy .
14 changes: 14 additions & 0 deletions umu/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[tool.mypy]
python_version = "3.10"
warn_return_any = true
ignore_missing_imports = true

disable_error_code = [
# Allow redefinitions since we redefine an error variable before raising exceptions
"no-redef"
]

exclude = [
'^umu_test\.py$',
'^umu_test_plugins\.py$',
]
8 changes: 4 additions & 4 deletions umu/umu_consts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from enum import Enum
from os import environ
from pathlib import Path


Expand Down Expand Up @@ -35,10 +35,10 @@ class Color(Enum):
"getnativepath",
}

FLATPAK_ID = environ.get("FLATPAK_ID") or ""
FLATPAK_ID = os.environ.get("FLATPAK_ID") or ""

FLATPAK_PATH: Path = (
Path(environ.get("XDG_DATA_HOME"), "umu") if FLATPAK_ID else None
FLATPAK_PATH: Path | None = (
Path(os.environ["XDG_DATA_HOME"], "umu") if FLATPAK_ID else None
)

UMU_LOCAL: Path = FLATPAK_PATH or Path.home().joinpath(
Expand Down
6 changes: 3 additions & 3 deletions umu/umu_log.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from logging import (
DEBUG,
ERROR,
Expand All @@ -9,7 +10,6 @@
StreamHandler,
getLogger,
)
from sys import stderr

from umu_consts import SIMPLE_FORMAT, Color

Expand All @@ -24,7 +24,7 @@ def console(self, msg: str) -> None:
Intended to be used to notify umu setup progress state for command
line usage
"""
print(f"{Color.BOLD.value}{msg}{Color.RESET.value}", file=stderr)
print(f"{Color.BOLD.value}{msg}{Color.RESET.value}", file=sys.stderr)


class CustomFormatter(Formatter): # noqa: D101
Expand All @@ -46,6 +46,6 @@ def format(self, record: LogRecord) -> str: # noqa: D102

log: CustomLogger = CustomLogger(getLogger(__name__))

console_handler: StreamHandler = StreamHandler(stream=stderr)
console_handler: StreamHandler = StreamHandler(stream=sys.stderr)
console_handler.setFormatter(CustomFormatter())
log.addHandler(console_handler)
36 changes: 22 additions & 14 deletions umu/umu_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

def set_env_toml(
env: dict[str, str], args: Namespace
) -> tuple[dict[str, str], list[str, tuple[str, Path]]]:
) -> tuple[dict[str, str], list[str]]:
"""Read key/values in a TOML file and map them to umu env. variables.
In the TOML file, certain keys map to environment variables:
Expand All @@ -27,32 +27,40 @@ def set_env_toml(
raise ModuleNotFoundError(err)

# User configuration containing required key/value pairs
toml: dict[str, Any] = None
toml: dict[str, Any]
# Configuration file path
config_path: Path
# Name of the configuration file
config: Path = Path(getattr(args, "config", None)).expanduser()
config: str = getattr(args, "config", "")
# Executable options, if any
opts: list[str] = []

if not config.is_file():
if not config:
err: str = f"Property 'config' does not exist in type '{type(args)}'"
raise AttributeError(err)

config_path = Path(config).expanduser()

if not config_path.is_file():
err: str = f"Path to configuration is not a file: '{config}'"
raise FileNotFoundError(err)

with config.open(mode="rb") as file:
with config_path.open(mode="rb") as file:
toml = tomllib.load(file)

_check_env_toml(toml)

# Required environment variables
env["WINEPREFIX"] = toml.get("umu").get("prefix")
env["PROTONPATH"] = toml.get("umu").get("proton")
env["EXE"] = toml.get("umu").get("exe")
env["WINEPREFIX"] = toml["umu"]["prefix"]
env["PROTONPATH"] = toml["umu"]["proton"]
env["EXE"] = toml["umu"]["exe"]
# Optional
env["GAMEID"] = toml.get("umu").get("game_id", "")
env["STORE"] = toml.get("umu").get("store", "")
env["GAMEID"] = toml["umu"].get("game_id", "")
env["STORE"] = toml["umu"].get("store", "")

if isinstance(toml.get("umu").get("launch_args"), list):
if isinstance(toml["umu"].get("launch_args"), list):
opts = toml["umu"]["launch_args"]
elif isinstance(toml.get("umu").get("launch_args"), str):
elif isinstance(toml["umu"].get("launch_args"), str):
opts = toml["umu"]["launch_args"].split(" ")

return env, opts
Expand All @@ -73,7 +81,7 @@ def _check_env_toml(toml: dict[str, Any]) -> dict[str, Any]:
raise ValueError(err)

for key in required_keys:
path: Path = None
path: Path

if key not in toml[table]:
err: str = (
Expand Down Expand Up @@ -102,7 +110,7 @@ def _check_env_toml(toml: dict[str, Any]) -> dict[str, Any]:
raise NotADirectoryError(err)

# Raise an error for empty values
for key, val in toml.get(table).items():
for key, val in toml[table].items():
if not val and isinstance(val, str):
err: str = (
f"Value is empty for '{key}'.\n"
Expand Down
69 changes: 35 additions & 34 deletions umu/umu_proton.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
from collections.abc import Callable
import os
import sys
from concurrent.futures import Future, ThreadPoolExecutor
from hashlib import sha512
from http.client import HTTPException
from json import loads
from os import environ
from pathlib import Path
from shutil import rmtree
from ssl import SSLContext, create_default_context
from sys import version
from tarfile import TarInfo
from tarfile import open as tar_open
from tempfile import mkdtemp
from urllib.request import Request, URLError, urlopen
from urllib.error import URLError
from urllib.request import Request, urlopen

from umu_consts import STEAM_COMPAT
from umu_log import log
from umu_util import run_zenity

SSL_DEFAULT_CONTEXT: SSLContext = create_default_context()
ssl_default_context: SSLContext = create_default_context()

try:
from tarfile import tar_filter

has_data_filter: bool = True
except ImportError:
tar_filter: Callable[[str, str], TarInfo] = None
has_data_filter: bool = False


def get_umu_proton(
Expand Down Expand Up @@ -59,7 +60,7 @@ def get_umu_proton(
if _get_from_steamcompat(env, STEAM_COMPAT) is env:
return env

environ["PROTONPATH"] = ""
os.environ["PROTONPATH"] = ""

return env

Expand All @@ -76,12 +77,12 @@ def _fetch_releases() -> list[tuple[str, str]]:
"User-Agent": "",
}

if environ.get("PROTONPATH") == "GE-Proton":
if os.environ.get("PROTONPATH") == "GE-Proton":
repo = "/repos/GloriousEggroll/proton-ge-custom/releases"

with urlopen( # noqa: S310
Request(f"{url}{repo}", headers=headers), # noqa: S310
context=SSL_DEFAULT_CONTEXT,
context=ssl_default_context,
) as resp:
if resp.status != 200:
return assets
Expand Down Expand Up @@ -152,7 +153,7 @@ def _fetch_proton(
# See https://github.com/astral-sh/ruff/issues/7918
log.console(f"Downloading {hash}...")
with (
urlopen(hash_url, context=SSL_DEFAULT_CONTEXT) as resp, # noqa: S310
urlopen(hash_url, context=ssl_default_context) as resp, # noqa: S310
):
if resp.status != 200:
err: str = (
Expand All @@ -167,7 +168,7 @@ def _fetch_proton(

# Proton
# Create a popup with zenity when the env var is set
if environ.get("UMU_ZENITY") == "1":
if os.environ.get("UMU_ZENITY") == "1":
bin: str = "curl"
opts: list[str] = [
"-LJO",
Expand All @@ -184,14 +185,14 @@ def _fetch_proton(
log.warning("zenity exited with the status code: %s", ret)
log.console("Retrying from Python...")

if not environ.get("UMU_ZENITY") or ret:
if not os.environ.get("UMU_ZENITY") or ret:
log.console(f"Downloading {tarball}...")
with (
urlopen( # noqa: S310
tar_url, context=SSL_DEFAULT_CONTEXT
tar_url, context=ssl_default_context
) as resp,
):
hash = sha512()
hashsum = sha512()

# Crash here because without Proton, the launcher will not work
if resp.status != 200:
Expand All @@ -207,9 +208,9 @@ def _fetch_proton(
view: memoryview = memoryview(buffer)
while size := resp.readinto(buffer):
file.write(view[:size])
hash.update(view[:size])
hashsum.update(view[:size])

if hash.hexdigest() != digest:
if hashsum.hexdigest() != digest:
err: str = f"Digest mismatched: {tarball}"
raise ValueError(err)

Expand All @@ -221,11 +222,11 @@ def _fetch_proton(
def _extract_dir(file: Path, steam_compat: Path) -> None:
"""Extract from a path to another location."""
with tar_open(file, "r:gz") as tar:
if tar_filter:
if has_data_filter:
log.debug("Using filter for archive")
tar.extraction_filter = tar_filter
else:
log.warning("Python: %s", version)
log.warning("Python: %s", sys.version)
log.warning("Using no data filter for archive")
log.warning("Archive will be extracted insecurely")

Expand Down Expand Up @@ -262,7 +263,7 @@ def _get_from_steamcompat(
"""
version: str = (
"GE-Proton"
if environ.get("PROTONPATH") == "GE-Proton"
if os.environ.get("PROTONPATH") == "GE-Proton"
else "UMU-Proton"
)

Expand All @@ -274,8 +275,8 @@ def _get_from_steamcompat(
)
log.console(f"{latest.name} found in: '{steam_compat}'")
log.console(f"Using {latest.name}")
environ["PROTONPATH"] = str(latest)
env["PROTONPATH"] = environ["PROTONPATH"]
os.environ["PROTONPATH"] = str(latest)
env["PROTONPATH"] = os.environ["PROTONPATH"]
except ValueError:
return None

Expand All @@ -300,11 +301,11 @@ def _get_latest(
$HOME/.local/share/Steam/compatibilitytool.d will be used.
"""
# Name of the Proton archive (e.g., GE-Proton9-7.tar.gz)
tarball: str = ""
tarball: str
# Name of the Proton directory (e.g., GE-Proton9-7)
proton: str = ""
proton: str
# Name of the Proton version, which is either UMU-Proton or GE-Proton
version: str = ""
version: str

if not assets:
return None
Expand All @@ -313,7 +314,7 @@ def _get_latest(
proton = tarball.removesuffix(".tar.gz")
version = (
"GE-Proton"
if environ.get("PROTONPATH") == "GE-Proton"
if os.environ.get("PROTONPATH") == "GE-Proton"
else "UMU-Proton"
)

Expand All @@ -322,8 +323,8 @@ def _get_latest(
log.console(f"{version} is up to date")
steam_compat.joinpath("UMU-Latest").unlink(missing_ok=True)
steam_compat.joinpath("UMU-Latest").symlink_to(proton)
environ["PROTONPATH"] = str(steam_compat.joinpath(proton))
env["PROTONPATH"] = environ["PROTONPATH"]
os.environ["PROTONPATH"] = str(steam_compat.joinpath(proton))
env["PROTONPATH"] = os.environ["PROTONPATH"]
return env

# Use the latest UMU/GE-Proton
Expand All @@ -343,8 +344,8 @@ def _get_latest(
future.result()
else:
_extract_dir(tmp.joinpath(tarball), steam_compat)
environ["PROTONPATH"] = str(steam_compat.joinpath(proton))
env["PROTONPATH"] = environ["PROTONPATH"]
os.environ["PROTONPATH"] = str(steam_compat.joinpath(proton))
env["PROTONPATH"] = os.environ["PROTONPATH"]
log.debug("Removing: %s", tarball)
thread_pool.submit(tmp.joinpath(tarball).unlink, True)
log.console(f"Using {version} ({proton})")
Expand Down Expand Up @@ -389,11 +390,11 @@ def _update_proton(
if not protons:
return

for proton in protons:
if proton.is_dir():
for stable in protons:
if stable.is_dir():
log.debug("Previous stable build found")
log.debug("Removing: %s", proton)
futures.append(thread_pool.submit(rmtree, str(proton)))
log.debug("Removing: %s", stable)
futures.append(thread_pool.submit(rmtree, str(stable)))

for _ in futures:
_.result()
Loading

0 comments on commit de17548

Please sign in to comment.