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

Static type checking / problem annotation / improvements / refactoring #152

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
Draft
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
6 changes: 3 additions & 3 deletions .github/scripts/check_verbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
import re
import subprocess

from glob import iglob
from pathlib import Path
from tempfile import mkdtemp
from collections.abc import Generator

# 'gui' is a virtual verb for opening the Winetricks GUI
# 'vd=1280x720' is a setting for the virtual desktop and valid
whitelist_verbs = {'gui', 'vd=1280x720'}


def extract_verbs_from_glob(path_glob: iglob) -> set[str]:
def extract_verbs_from_glob(path_glob: Generator[Path, None, None]) -> set[str]:
"""Simply strip the extension from all found files."""
return {file.stem for file in path_glob}

Expand Down Expand Up @@ -48,7 +48,7 @@ def find_valid_verbs(root: Path) -> set[str]:

# Setup environment variables
env = os.environ.copy()
env['TMPDIR'] = tmp_dir
env['TMPDIR'] = str(tmp_dir)
env['WINETRICKS_LATEST_VERSION_CHECK'] = 'disabled'

# Execute winetricks, suppress output
Expand Down
34 changes: 24 additions & 10 deletions .github/scripts/steam_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,60 @@
from steam.utils.proto import proto_to_dict
from steam.core.connection import WebsocketConnection

class Steam: # noqa: D101
def __init__(self) -> None: # noqa: D107
class Steam:
"""Minimal implementation of the SteamClient package that allows app id validation"""

def __init__(self) -> None:
"""Setup SteamClient and it's events

Raises:
ValueError: When the SteamClient fires it's "error" event
"""
self.logged_on_once = False

self.steam = client = SteamClient()
client.connection = WebsocketConnection()

@client.on('error')
# FIXME: pyright outputs 'error: Object of type "None" cannot be called (reportOptionalCall)'
@client.on(SteamClient.EVENT_ERROR) # pyright: ignore (reportOptionalCall)
def handle_error(result: EResult) -> None:
raise ValueError(f'Steam error: {repr(result)}')

@client.on('connected')
@client.on(SteamClient.EVENT_CONNECTED) # pyright: ignore (reportOptionalCall)
def handle_connected() -> None:
print(f'Connected to {client.current_server_addr}', file=sys.stderr)

@client.on('channel_secured')
@client.on(SteamClient.EVENT_CHANNEL_SECURED) # pyright: ignore (reportOptionalCall)
def send_login() -> None:
if self.logged_on_once and self.steam.relogin_available:
self.steam.relogin()

@client.on('disconnected')
@client.on(SteamClient.EVENT_DISCONNECTED) # pyright: ignore (reportOptionalCall)
def handle_disconnect() -> None:
print('Steam disconnected', file=sys.stderr)
if self.logged_on_once:
print('Reconnecting...', file=sys.stderr)
client.reconnect(maxdelay=30)

@client.on('logged_on')
@client.on(SteamClient.EVENT_LOGGED_ON) # pyright: ignore (reportOptionalCall)
def handle_after_logon() -> None:
self.logged_on_once = True

client.anonymous_login()


def get_valid_appids(self, appids: set[int]) -> set[int]:
"""Queries Steam for the specified appids.

If an appid doesn't exist, it won't be in the response.
Args:
appids (set[int]): The app ids that should be validated

Raises:
ValueError: When the response is empty / unexpected

Raises a ValueError if Steam returns unexpected data
"""
Returns:
set[int]: Only valid app ids will be returned
"""
# https://github.com/SteamRE/SteamKit/blob/master/SteamKit2/SteamKit2/Base/Generated/SteamMsgClientServerAppInfo.cs#L331
resp = self.steam.send_job_and_wait(
message = MsgProto(EMsg.ClientPICSProductInfoRequest),
Expand Down
32 changes: 23 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,42 @@ jobs:
with:
submodules: recursive
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
# The Steam Runtime platform (sniper) uses Python 3.9
python-version: "3.9"
- name: Install dependencies
cache: 'pip' # caching pip dependencies

- name: Install Python dependencies
run: |
sudo apt-get install shellcheck
python3 -m pip install --upgrade pip
pip install ruff
pip install ijson
pip install "git+https://github.com/njbooher/steam.git@wsproto#egg=steam[client]"
- name: Lint with Shellcheck
run: |
shellcheck winetricks

# FIXME: problem matcher is currently disabled upstream, using a fork for the moment
# https://github.com/ludeeus/action-shellcheck/pull/103
# FIXME: symlinks don't work upstream
# https://github.com/ludeeus/action-shellcheck/pull/104
- name: Run ShellCheck
uses: Root-Core/action-shellcheck@fork
with:
ignore_paths: subprojects # prevent ShellCheck from checking unrelated files
ignore_symlinks: false # winetricks is symlinked

# Ruff uses ruff.toml for it's configuration
- name: Lint with Ruff
run: |
ruff check .
uses: astral-sh/ruff-action@v1

# Pyright uses pyproject.toml for it's configuration
- name: Static type checking with Pyright
uses: jakebailey/pyright-action@v2

- name: Validate gamefix modules
run: |
python3 .github/scripts/check_gamefixes.py
python3 .github/scripts/check_verbs.py

- name: Test with unittest
run: |
python3 protonfixes_test.py
59 changes: 25 additions & 34 deletions config.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,37 @@
"""Load configuration settings for protonfixes"""

import os
from configparser import ConfigParser
from config_base import ConfigBase
from dataclasses import dataclass
from pathlib import Path

try:
from .logger import log
except ImportError:
from logger import log
class Config(ConfigBase):
"""Configuration for umu-protonfix"""

@dataclass
class MainSection:
"""General parameters

Attributes:
enable_checks (bool): Run checks (`checks.py`) before the fix is executed.
enable_global_fixes (bool): Enables included fixes. If deactivated, only local fixes (`~/.config/protonfixes/localfixes`) are executed.

CONF_FILE = '~/.config/protonfixes/config.ini'
DEFAULT_CONF = """
[main]
enable_checks = true
enable_splash = false
enable_global_fixes = true
"""

enable_checks: bool = True
enable_global_fixes: bool = True

[path]
cache_dir = ~/.cache/protonfixes
"""
@dataclass
class PathSection:
"""Path parameters

CONF = ConfigParser()
CONF.read_string(DEFAULT_CONF)
Attributes:
cache_dir (Path): The path that should be used to create temporary and cached files.

try:
CONF.read(os.path.expanduser(CONF_FILE))
"""

except Exception:
log.debug('Unable to read config file ' + CONF_FILE)
cache_dir: Path = Path.home() / '.cache/protonfixes'

main: MainSection
path: PathSection

def opt_bool(opt: str) -> bool:
"""Convert bool ini strings to actual boolean values"""
return opt.lower() in ['yes', 'y', 'true', '1']


locals().update({x: opt_bool(y) for x, y in CONF['main'].items() if 'enable' in x})

locals().update({x: os.path.expanduser(y) for x, y in CONF['path'].items()})

try:
[os.makedirs(os.path.expanduser(d)) for n, d in CONF['path'].items()]
except OSError:
pass
config = Config(Path.home() / '.config/protonfixes/config.ini')
Loading