Skip to content

Commit 5f3175b

Browse files
committed
Start working on automatic README generation.
1 parent da2f4dc commit 5f3175b

6 files changed

Lines changed: 263 additions & 1 deletion

File tree

.pre-commit-config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ repos:
2828
"-o",
2929
"plugin-requirements.txt",
3030
]
31+
- repo: local
32+
hooks:
33+
- id: update-readme-games
34+
name: Update list of games in README.md
35+
entry: python
36+
language: python
37+
# files: ^(.*/)?\.py$
38+
args: [pre-commit-hooks/update-readme-games.py]
39+
additional_dependencies: [jinja2==3.1.6]
3140

3241
ci:
3342
autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks."

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic
4545

4646
## Supported games
4747

48+
<!-- START: GAMES -->
49+
4850
| Game | Author | File | Extras |
4951
|------|--------|------|--------|
5052
| The Binding of Isaac: Rebirth — [STEAM](https://store.steampowered.com/app/250900/The_Binding_of_Isaac_Rebirth/) |[EzioTheDeadPoet](https://github.com/EzioTheDeadPoet)|[game_thebindingofisaacrebirth.py](games/game_thebindingofisaacrebirth.py)|<ul><li>profile specific ini file</li></ul>|
@@ -82,6 +84,8 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic
8284
| Yu-Gi-Oh! Master Duel — [STEAM](https://store.steampowered.com/app/1449850/) | [The Conceptionist](https://github.com/the-conceptionist) & [uwx](https://github.com/uwx) | [game_masterduel.py](games/game_masterduel.py) | |
8385
| Zeus and Poseidon — [GOG](https://www.gog.com/game/zeus_poseidon) / [STEAM](https://store.steampowered.com/app/566050/Zeus__Poseidon/) | [Holt59](https://github.com/holt59/) | [game_zeusandpoiseidon.py](games/game_zeusandpoiseidon.py) | <ul><li>mod data checker</li></ul> |
8486

87+
<!-- END: GAMES -->
88+
8589
## How to add a new game?
8690

8791
You can create a plugin by providing a python class in the `games` folder.

poetry.lock

Lines changed: 90 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{%- for game in games -%}
2+
| {{ game.name }} | {%- for author in game.authors -%}
3+
{%- if author.homepage -%}
4+
[{{ author.name }}]({{ author.homepage }})
5+
{%- else -%}
6+
{{ author.name }}
7+
{%- endif -%}
8+
{%- if not loop.last %}, {% endif %}
9+
{%- endfor -%} | [{{ game.file.split('/') | last }}]({{ game.file }}) | |
10+
{% endfor %}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import ast
2+
import logging
3+
from pathlib import Path
4+
from typing import TypedDict, cast
5+
6+
from jinja2 import Template
7+
8+
START_TAG = "<!-- START: GAMES -->"
9+
END_TAG = "<!-- END: GAMES -->"
10+
11+
_FILE = Path(__file__)
12+
ROOT = _FILE.parent.parent
13+
TPLT = _FILE.with_suffix(".jinja")
14+
15+
# careful with the new lines here
16+
HEADER = """
17+
<!-- This section was automatically generated, do not edit! -->
18+
19+
| Game | Author | File | Extras |
20+
|------|--------|------|--------|
21+
"""
22+
FOOTER = """
23+
"""
24+
25+
CUSTOM_AUTHOR_HOMEPAGES = {
26+
"Luca/EzioTheDeadPoet": "https://eziothedeadpoet.github.io/AboutMe/",
27+
"R3z Shark": "",
28+
"Miner Of Worlds": "",
29+
"Kane Dou": "",
30+
"Ryan Young": "",
31+
"The Conceptionist": "",
32+
}
33+
34+
35+
class Author(TypedDict):
36+
name: str
37+
homepage: str
38+
39+
40+
class BasicGameInformation(TypedDict, total=False):
41+
name: str
42+
authors: list[Author]
43+
file: str
44+
45+
46+
def extract_basic_games(path: Path):
47+
logging.info(f"extracting basic games from {path}...")
48+
with open(path, "r") as fp:
49+
module = ast.parse(fp.read())
50+
51+
games: list[ast.ClassDef] = []
52+
for stmt in module.body:
53+
if not isinstance(stmt, ast.ClassDef):
54+
continue
55+
56+
for base in stmt.bases:
57+
if isinstance(base, ast.Name) and base.id == "BasicGame":
58+
games.append(stmt)
59+
break
60+
61+
# not sure if this is possible? would mean something like xxx.BasicGame
62+
if isinstance(base, ast.Attribute) and base.attr == "BasicGame":
63+
games.append(stmt)
64+
break
65+
66+
return games
67+
68+
69+
def extract_basic_game_information(path: Path, game: ast.ClassDef):
70+
value: BasicGameInformation = {
71+
"file": "https://github.com/ModOrganizer/modorganizer-basic_games/blob/master/"
72+
+ path.relative_to(ROOT).as_posix()
73+
}
74+
for stmt in game.body:
75+
if not isinstance(stmt, ast.Assign):
76+
continue
77+
78+
# skip multiple assignments (should not be any in a class?)
79+
if len(stmt.targets) != 1:
80+
continue
81+
82+
target = stmt.targets[0]
83+
if not isinstance(target, ast.Name): # can this happen?
84+
continue
85+
86+
match target.id:
87+
case "GameName":
88+
assert isinstance(stmt.value, ast.Constant)
89+
value["name"] = stmt.value.value
90+
case "Author":
91+
assert isinstance(stmt.value, ast.Constant)
92+
author_s = cast(str, stmt.value.value)
93+
94+
authors = [p.strip() for p in author_s.split(",")]
95+
authors = [p.strip() for pp in authors for p in pp.split("&")]
96+
value["authors"] = sorted(
97+
[
98+
{
99+
"name": author,
100+
"homepage": CUSTOM_AUTHOR_HOMEPAGES.get(
101+
author, f"https://github.com/{author}"
102+
),
103+
}
104+
for author in authors
105+
],
106+
key=lambda a: a["name"],
107+
)
108+
case _:
109+
pass
110+
111+
print(stmt, stmt.value, target.id)
112+
return value
113+
114+
115+
def generate_table():
116+
# list the games
117+
games: list[tuple[Path, ast.ClassDef]] = []
118+
for path in ROOT.joinpath("games").glob("**/*.py"):
119+
if path.parent.name == "quarantine":
120+
continue
121+
122+
games.extend((path, game) for game in extract_basic_games(path))
123+
124+
infos = [extract_basic_game_information(path, game) for path, game in games]
125+
126+
with open(TPLT, "r") as fp:
127+
template = Template(fp.read())
128+
129+
content = template.render(games=sorted(infos, key=lambda g: g.get("name", "")))
130+
131+
return HEADER + content + FOOTER
132+
133+
134+
if __name__ == "__main__":
135+
logging.basicConfig(level=logging.INFO)
136+
137+
# read the README content
138+
with open(ROOT.joinpath("README.md"), "r") as fp:
139+
readme = fp.read()
140+
141+
# find the start and end block
142+
start = readme.find(START_TAG)
143+
end = readme.find(END_TAG)
144+
assert start >= 0 and end >= 0
145+
146+
readme = readme[:start] + START_TAG + "\n" + generate_table() + readme[end:]
147+
148+
with open(ROOT.joinpath("README2.md"), "w") as fp:
149+
fp.write(readme)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pyright = "^1.1.400"
2828
ruff = "^0.11.7"
2929
types-psutil = "^5.9.5.20240516"
3030
poethepoet = "^0.34.0"
31+
jinja2 = "3.1.6"
3132

3233
[build-system]
3334
requires = ["poetry-core (>=2.0)"]

0 commit comments

Comments
 (0)