From b9d034eef78282e65fdab787531856cac5c40b1d Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 10:49:51 +0300 Subject: [PATCH 01/10] Project: Add pylint configuration file --- .github/workflows/checks.yml | 2 +- pylintrc | 643 +++++++++++++++++++++++++++++++++++ 2 files changed, 644 insertions(+), 1 deletion(-) create mode 100644 pylintrc diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5c4b72e0..ccb1920d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -47,4 +47,4 @@ jobs: pip3 install qstylizer - name: Analysis with pylint run: | - python3 -m pylint -E rare --jobs=3 --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5 --generated-members=PyQt5.*,orjson.* + python3 -m pylint rare diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..71caea94 --- /dev/null +++ b/pylintrc @@ -0,0 +1,643 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +errors-only=yes + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist=PyQt5 + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=ui,singleton.py + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked and +# will not be imported (useful for modules/projects where namespaces are +# manipulated during runtime and thus existing member attributes cannot be +# deduced by static analysis). It supports qualified module names, as well as +# Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=3 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.9 + +# Discover python modules and packages in the file system subtree. +recursive=yes + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, + no-name-in-module, + unexpected-keyword-arg, + no-value-for-parameter + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + +# Let 'consider-using-join' be raised when the separator to join on would be +# non-empty (resulting in expected fixes of the type: ``"- " + " - +# ".join(items)``) +suggest-join-with-non-empty-separator=yes + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members=PyQt5.*,orjson.* + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io From 1d8ae15ea95d4810a64351568cf57c868635f655 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 13:15:48 +0300 Subject: [PATCH 02/10] SteamGrades: Fix exception when trying to get file timestamp if the file does not exist. At the same time fix some pylint warnings. --- rare/utils/steam_grades.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py index 69913196..ce73fde8 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -1,7 +1,8 @@ import difflib import os from datetime import datetime -from enum import StrEnum, Enum +from enum import Enum +from logging import getLogger from typing import Tuple import orjson @@ -10,6 +11,8 @@ import requests from rare.lgndr.core import LegendaryCore from rare.utils.paths import cache_dir +logger = getLogger("SteamGrades") + replace_chars = ",;.:-_ " steamapi_url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/" protondb_url = "https://www.protondb.com/api/v1/reports/summaries/" @@ -52,6 +55,7 @@ def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]: raise Exception grade = get_grade(steam_id) except Exception as e: + logger.exception(e) return 0, "fail" else: return steam_id, grade @@ -64,28 +68,31 @@ def get_grade(steam_code): steam_code = str(steam_code) res = requests.get(f"{protondb_url}{steam_code}.json") try: - lista = orjson.loads(res.text) - except orjson.JSONDecodeError: + app = orjson.loads(res.text) + except orjson.JSONDecodeError as e: + logger.exception(e) return "fail" - return lista["tier"] + return app.get("tier", "fail") def load_json() -> dict: - file = os.path.join(cache_dir(), "game_list.json") - mod_time = datetime.fromtimestamp(os.path.getmtime(file)) - elapsed_time = abs(datetime.now() - mod_time) global __active_download if __active_download: return {} - if not os.path.exists(file) or elapsed_time.days > 7: + file = os.path.join(cache_dir(), "steam_appids.json") + elapsed_days = 0 + if os.path.exists(file): + mod_time = datetime.fromtimestamp(os.path.getmtime(file)) + elapsed_days = abs(datetime.now() - mod_time).days + if not os.path.exists(file) or elapsed_days > 7: __active_download = True response = requests.get(steamapi_url) __active_download = False - steam_ids = orjson.loads(response.text)["applist"]["apps"] + apps = orjson.loads(response.text).get("applist", {}).get("apps", {}) ids = {} - for game in steam_ids: - ids[game["name"]] = game["appid"] + for app in apps: + ids[app["name"]] = app["appid"] with open(file, "w") as f: f.write(orjson.dumps(ids).decode("utf-8")) return ids From 07fa7890cfb1b474a900788b9f457af45a3e685a Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 13:18:05 +0300 Subject: [PATCH 03/10] RareGame: Update protondb grade every 3 days even if it already exists. --- rare/models/game.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/rare/models/game.py b/rare/models/game.py index bab7115a..f38fa6ac 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -41,11 +41,11 @@ class RareGame(RareGameSlim): return cls( queued=data.get("queued", False), queue_pos=data.get("queue_pos", None), - last_played=datetime.fromisoformat(x) if (x := data.get("last_played", None)) else datetime.min, - grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", None)) else datetime.min, + last_played=datetime.fromisoformat(x) if (x := data.get("last_played", "")) else datetime.min, + grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", "")) else datetime.min, steam_appid=data.get("steam_appid", None), steam_grade=data.get("steam_grade", None), - steam_date=datetime.fromisoformat(x) if (x := data.get("steam_date", None)) else datetime.min, + steam_date=datetime.fromisoformat(x) if (x := data.get("steam_date", "")) else datetime.min, tags=data.get("tags", []), ) @@ -432,10 +432,13 @@ class RareGame(RareGameSlim): if self.metadata.steam_grade != "pending": elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date) - if elapsed_time.days > 3 and (self.metadata.steam_grade is None or self.metadata.steam_appid is None): + if elapsed_time.days > 3: + logger.info("Refreshing ProtonDB grade for %s", self.app_title) + def _set_steam_grade(): appid, rating = get_rating(self.core, self.app_name) self.set_steam_grade(appid, rating) + worker = QRunnable.create(_set_steam_grade) QThreadPool.globalInstance().start(worker) self.metadata.steam_grade = "pending" @@ -445,12 +448,15 @@ class RareGame(RareGameSlim): def steam_appid(self) -> Optional[int]: return self.metadata.steam_appid + def set_steam_appid(self, appid: int): + set_envvar(self.app_name, "SteamAppId", str(appid)) + set_envvar(self.app_name, "SteamGameId", str(appid)) + set_envvar(self.app_name, "STEAM_COMPAT_APP_ID", str(appid)) + self.metadata.steam_appid = appid + def set_steam_grade(self, appid: int, grade: str) -> None: if appid and self.steam_appid is None: - set_envvar(self.app_name, "SteamAppId", str(appid)) - set_envvar(self.app_name, "SteamGameId", str(appid)) - set_envvar(self.app_name, "STEAM_COMPAT_APP_ID", str(appid)) - self.metadata.steam_appid = appid + self.set_steam_appid(appid) self.metadata.steam_grade = grade self.metadata.steam_date = datetime.utcnow() self.__save_metadata() From 4497a1c712567390c758c8a3fd08fc744666408e Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 13:32:00 +0300 Subject: [PATCH 04/10] Project: Use `vdf` package on all platforms Useful on Windows too, to export games to Steam as "Non-Steam Game" --- rare/utils/compat/steam.py | 99 ++++++++++++++++++++++---------------- requirements-flatpak.txt | 1 + requirements-full.txt | 2 +- requirements.txt | 2 +- 4 files changed, 61 insertions(+), 43 deletions(-) diff --git a/rare/utils/compat/steam.py b/rare/utils/compat/steam.py index ec4428c4..3a539cb4 100644 --- a/rare/utils/compat/steam.py +++ b/rare/utils/compat/steam.py @@ -1,4 +1,3 @@ -import platform as pf import os import shlex from dataclasses import dataclass @@ -7,11 +6,9 @@ from hashlib import md5 from logging import getLogger from typing import Optional, Union, List, Dict, Set -if pf.system() in {"Linux", "FreeBSD"}: - # noinspection PyUnresolvedReferences - import vdf # pylint: disable=E0401 +import vdf -logger = getLogger("Proton") +logger = getLogger("SteamTools") steam_compat_client_install_paths = [os.path.expanduser("~/.local/share/Steam")] @@ -149,6 +146,32 @@ def find_appmanifests(library: str) -> List[dict]: return appmanifests +def find_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]: + runtimes = {} + appmanifests = find_appmanifests(library) + common = os.path.join(library, "common") + for appmanifest in appmanifests: + folder = appmanifest["AppState"]["installdir"] + tool_path = os.path.join(common, folder) + if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): + with open(vdf_file, "r") as f: + toolmanifest = vdf.load(f) + if toolmanifest["manifest"]["compatmanager_layer_name"] == "container-runtime": + print(toolmanifest["manifest"]) + runtimes.update( + { + appmanifest["AppState"]["appid"]: SteamRuntime( + steam_path=steam_path, + steam_library=library, + appmanifest=appmanifest, + tool_path=tool_path, + toolmanifest=toolmanifest["manifest"], + ) + } + ) + return runtimes + + def find_protons(steam_path: str, library: str) -> List[ProtonTool]: protons = [] appmanifests = find_appmanifests(library) @@ -225,33 +248,7 @@ def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: return tools -def find_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]: - runtimes = {} - appmanifests = find_appmanifests(library) - common = os.path.join(library, "common") - for appmanifest in appmanifests: - folder = appmanifest["AppState"]["installdir"] - tool_path = os.path.join(common, folder) - if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): - with open(vdf_file, "r") as f: - toolmanifest = vdf.load(f) - if toolmanifest["manifest"]["compatmanager_layer_name"] == "container-runtime": - print(toolmanifest["manifest"]) - runtimes.update( - { - appmanifest["AppState"]["appid"]: SteamRuntime( - steam_path=steam_path, - steam_library=library, - appmanifest=appmanifest, - tool_path=tool_path, - toolmanifest=toolmanifest["manifest"], - ) - } - ) - return runtimes - - -def find_runtime( +def get_runtime( tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, SteamRuntime] ) -> Optional[SteamRuntime]: required_tool = tool.required_tool @@ -305,7 +302,7 @@ def get_steam_environment( return environ -def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: +def _find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: steam_path = find_steam() if steam_path is None: logger.info("Steam could not be found") @@ -326,7 +323,7 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: tools.extend(find_compatibility_tools(steam_path)) for tool in tools: - runtime = find_runtime(tool, runtimes) + runtime = get_runtime(tool, runtimes) tool.runtime = runtime tools = list(filter(lambda t: bool(t), tools)) @@ -334,14 +331,34 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: return tools +_tools: Optional[List[Union[ProtonTool, CompatibilityTool]]] = None + + +def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: + global _tools + if _tools is None: + _tools = _find_tools() + return list(filter(lambda t: t.layer != "umu-launcher", _tools)) + + +def find_umu_launcher() -> Optional[CompatibilityTool]: + global _tools + if _tools is None: + _tools = _find_tools() + _umu = list(filter(lambda t: t.layer == "umu-launcher", _tools)) + return _umu[0] if _umu else None + + if __name__ == "__main__": from pprint import pprint - _tools = find_tools() - pprint(_tools) + tools = find_tools() + pprint(tools) + umu = find_umu_launcher() + pprint(umu) - for _tool in _tools: - print(get_steam_environment(_tool)) - print(_tool.name) - print(_tool.command(SteamVerb.RUN)) - print(" ".join(_tool.command(SteamVerb.RUN_IN_PREFIX))) + for tool in tools: + print(get_steam_environment(tool)) + print(tool.name) + print(tool.command(SteamVerb.RUN)) + print(" ".join(tool.command(SteamVerb.RUN_IN_PREFIX))) diff --git a/requirements-flatpak.txt b/requirements-flatpak.txt index 696d1426..c4e89fd3 100644 --- a/requirements-flatpak.txt +++ b/requirements-flatpak.txt @@ -3,4 +3,5 @@ QtAwesome setuptools legendary-gl>=0.20.34 # orjson # needs the binary release, use req2flatpak +vdf pypresence diff --git a/requirements-full.txt b/requirements-full.txt index a2754f33..2556e3e3 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -5,7 +5,7 @@ setuptools legendary-gl>=0.20.34; platform_system != "Windows" or platform_system != "Darwin" legendary-gl @ git+https://github.com/derrod/legendary@96e07ff ; platform_system == "Windows" or platform_system == "Darwin" orjson -vdf; platform_system != "Windows" +vdf pywin32; platform_system == "Windows" pywebview[qt]; platform_system == "Linux" pywebview[qt]; platform_system == "FreeBSD" diff --git a/requirements.txt b/requirements.txt index a83fe85c..c91823c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ setuptools legendary-gl>=0.20.34; platform_system != "Windows" or platform_system != "Darwin" legendary-gl @ git+https://github.com/derrod/legendary@96e07ff ; platform_system == "Windows" or platform_system == "Darwin" orjson -vdf; platform_system == "Linux" or platform_system == "FreeBSD" +vdf pywin32; platform_system == "Windows" From 337d753d0cca3a539bff27e82468711af9a0e7e9 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 13:38:16 +0300 Subject: [PATCH 05/10] Workflows: Disable fail-fast for pylint checks --- .github/workflows/checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index ccb1920d..d4c8cc68 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -23,6 +23,7 @@ on: jobs: pylint: strategy: + fail-fast: false matrix: os: ['macos-latest', 'windows-latest', 'ubuntu-latest'] version: ['3.9', '3.10', '3.11', '3.12'] From beb4f6c31056798e62b92dc94355a291150cccb6 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 13:57:30 +0300 Subject: [PATCH 06/10] Workflows: Update Windows packages to python 3.12 Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com> --- .github/workflows/job_cx-freeze-msi.yml | 2 +- .github/workflows/job_cx-freeze-zip.yml | 2 +- .github/workflows/job_version.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/job_cx-freeze-msi.yml b/.github/workflows/job_cx-freeze-msi.yml index f281d005..2a13f089 100644 --- a/.github/workflows/job_cx-freeze-msi.yml +++ b/.github/workflows/job_cx-freeze-msi.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-python@v4 with: cache: pip - python-version: '3.11' + python-version: '3.12' check-latest: true architecture: x64 - name: Install Build Dependencies diff --git a/.github/workflows/job_cx-freeze-zip.yml b/.github/workflows/job_cx-freeze-zip.yml index 4fa80d6e..0398e0f3 100644 --- a/.github/workflows/job_cx-freeze-zip.yml +++ b/.github/workflows/job_cx-freeze-zip.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-python@v4 with: cache: pip - python-version: '3.11' + python-version: '3.12' check-latest: true architecture: x64 - name: Install build dependencies diff --git a/.github/workflows/job_version.yml b/.github/workflows/job_version.yml index 900dd8a9..c7a53a4b 100644 --- a/.github/workflows/job_version.yml +++ b/.github/workflows/job_version.yml @@ -4,9 +4,9 @@ on: workflow_call: outputs: version: - value: ${{ jobs.version.outputs.tag_abbrev }}.${{ jobs.version.outputs.tag_offset }} + value: "${{ jobs.version.outputs.tag_abbrev }}.${{ jobs.version.outputs.tag_offset }}" branch: - value: ${{ jobs.version.outputs.branch }} + value: "${{ jobs.version.outputs.branch }}" jobs: From 4bb1fb10eeb3c22c2bbaad896d3fbcf80b916d1e Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 13:58:10 +0300 Subject: [PATCH 07/10] SteamGrades: Print an error message instead of the exception --- rare/utils/steam_grades.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py index ce73fde8..49b60289 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -54,8 +54,8 @@ def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]: if not steam_id: raise Exception grade = get_grade(steam_id) - except Exception as e: - logger.exception(e) + except Exception: + logger.error("Failed to get ProtonDB rating for %s", game.app_title) return 0, "fail" else: return steam_id, grade @@ -70,7 +70,7 @@ def get_grade(steam_code): try: app = orjson.loads(res.text) except orjson.JSONDecodeError as e: - logger.exception(e) + logger.error("Failed to get ProtonDB response for %s", steam_code) return "fail" return app.get("tier", "fail") From 9017826b1640af7687872fdd55a9412b88b01bcf Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 13:58:59 +0300 Subject: [PATCH 08/10] StoreAPI: Log exception --- rare/components/tabs/store/store_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rare/components/tabs/store/store_api.py b/rare/components/tabs/store/store_api.py index bfdff865..112be2e8 100644 --- a/rare/components/tabs/store/store_api.py +++ b/rare/components/tabs/store/store_api.py @@ -97,13 +97,13 @@ class StoreAPI(QObject): except KeyError as e: if DEBUG(): raise e - logger.error("Free games Api request failed") + logger.exception("Free games API request failed") handle_func(["error", "Key error"]) return except Exception as e: if DEBUG(): raise e - logger.error(f"Free games Api request failed: {e}") + logger.exception(f"Free games API request failed") handle_func(["error", e]) return From f9cc1b48f1224005d51ed41aa1761bd635d2bce2 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 19:27:59 +0300 Subject: [PATCH 09/10] GameSettings: Fix pylint errors (`possibly-used-before-assignment`) --- rare/components/tabs/settings/settings.py | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/rare/components/tabs/settings/settings.py b/rare/components/tabs/settings/settings.py index a974aa3e..020f4eec 100644 --- a/rare/components/tabs/settings/settings.py +++ b/rare/components/tabs/settings/settings.py @@ -9,10 +9,9 @@ from .widgets.wrappers import WrapperSettings if pf.system() != "Windows": from .widgets.wine import WineSettings - -if pf.system() in {"Linux", "FreeBSD"}: - from .widgets.proton import ProtonSettings - from .widgets.overlay import MangoHudSettings + if pf.system() in {"Linux", "FreeBSD"}: + from .widgets.proton import ProtonSettings + from .widgets.overlay import MangoHudSettings logger = getLogger("GameSettings") @@ -24,18 +23,19 @@ class LaunchSettings(LaunchSettingsBase): class GameSettings(GameSettingsBase): def __init__(self, parent=None): - if pf.system() in {"Linux", "FreeBSD"}: - super(GameSettings, self).__init__( - LaunchSettings, DxvkSettings, EnvVars, - WineSettings, ProtonSettings, MangoHudSettings, - parent=parent - ) - elif pf.system() != "Windows": - super(GameSettings, self).__init__( - LaunchSettings, DxvkSettings, EnvVars, - WineSettings, - parent=parent - ) + if pf.system() != "Windows": + if pf.system() in {"Linux", "FreeBSD"}: + super(GameSettings, self).__init__( + LaunchSettings, DxvkSettings, EnvVars, + WineSettings, ProtonSettings, MangoHudSettings, + parent=parent + ) + else: + super(GameSettings, self).__init__( + LaunchSettings, DxvkSettings, EnvVars, + WineSettings, + parent=parent + ) else: super(GameSettings, self).__init__( LaunchSettings, DxvkSettings, EnvVars, From 3b721fdd13b62e72aaca6a3611571f437ea4db2e Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 16 May 2024 19:29:14 +0300 Subject: [PATCH 10/10] WinePathResolver: Disable pylint check (`possibly-used-before-assignment`) --- rare/shared/workers/wine_resolver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rare/shared/workers/wine_resolver.py b/rare/shared/workers/wine_resolver.py index 17465314..892b337c 100644 --- a/rare/shared/workers/wine_resolver.py +++ b/rare/shared/workers/wine_resolver.py @@ -55,6 +55,7 @@ class WinePathResolver(Worker): disable_wine=config.get_boolean(app_name, "no_wine") ) env = core.get_app_environment(app_name, disable_wine=config.get_boolean(app_name, "no_wine")) + # pylint: disable=E0606 env = compat_utils.get_host_environment(env, silent=True) return cmd, env