Skip to content

Instantly share code, notes, and snippets.

@adamscott
Last active February 25, 2025 19:20
Show Gist options
  • Save adamscott/b00deead1abccad38cc97e68febe0f34 to your computer and use it in GitHub Desktop.
Save adamscott/b00deead1abccad38cc97e68febe0f34 to your computer and use it in GitHub Desktop.
Godot API introduction parser

How to run this

  1. Copy the requirements.txt file locally
  2. Run pip install -r /path/to/requirements.txt to install the dependencies.
  3. Copy the godot-api-intro.py file locally
  4. Run python /path/to/godot-api-intro.py init to initialize the local sqlite3 database (godot_version_doc.db)
  5. Run python /path/to/godot-api-intro.py parse /path/to/godot/repo <GODOT_VERSION> to enter data from that version.
  • The script will checkout the version specified.
#!/usr/bin/env python
import argparse
import sqlite3
import pathlib
import xml.etree.ElementTree as ET
import subprocess
import sys
import semver
GODOT_DB = "godot_version_doc.db"
db_connection: sqlite3.Connection | None = None
def is_tool(name: str) -> bool:
from shutil import which
return which(name) is not None
def print_error(*args, **kwargs):
print(*args, **kwargs, file=sys.stderr)
def connect_to_db() -> sqlite3.Connection:
global GODOT_DB
global db_connection
db_connection = sqlite3.connect(GODOT_DB)
return db_connection
def init_db(force: bool):
global GODOT_DB
if force:
godot_db = pathlib.Path(GODOT_DB)
if godot_db.exists():
godot_db.unlink()
cursor = connect_to_db().cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS classes (
class_id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
UNIQUE (name, path)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS constructors(
constructor_id INTEGER PRIMARY KEY NOT NULL,
class_id INTEGER,
name TEXT NOT NULL,
UNIQUE (class_id, name),
FOREIGN KEY (class_id)
REFERENCES classes (class_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS methods (
method_id INTEGER PRIMARY KEY NOT NULL,
class_id INTEGER,
name TEXT NOT NULL,
UNIQUE (class_id, name),
FOREIGN KEY (class_id)
REFERENCES classes (class_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS members (
member_id INTEGER PRIMARY KEY NOT NULL,
class_id INTEGER,
name TEXT NOT NULL,
UNIQUE (class_id, name),
FOREIGN KEY (class_id)
REFERENCES classes (class_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS constants (
constant_id INTEGER PRIMARY KEY NOT NULL,
class_id INTEGER,
name TEXT NOT NULL,
UNIQUE (class_id, name),
FOREIGN KEY (class_id)
REFERENCES classes (class_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS signals (
signal_id INTEGER PRIMARY KEY NOT NULL,
class_id INTEGER,
name TEXT NOT NULL,
UNIQUE (class_id, name),
FOREIGN KEY (class_id)
REFERENCES classes (class_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS operators (
operator_id INTEGER PRIMARY KEY NOT NULL,
class_id INTEGER,
name TEXT NOT NULL,
UNIQUE (class_id, name),
FOREIGN KEY (class_id)
REFERENCES classes (class_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS versions (
version_id INTEGER PRIMARY KEY NOT NULL,
major INTEGER,
minor INTEGER,
patch INTEGER,
UNIQUE (major, minor, patch)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS class_versions (
class_id INTEGER,
version_id INTEGER,
UNIQUE (class_id, version_id),
PRIMARY KEY (class_id, version_id)
FOREIGN KEY (class_id)
REFERENCES classes (class_id)
ON DELETE CASCADE
ON UPDATE NO ACTION,
FOREIGN KEY (version_id)
REFERENCES versions (version_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS constructor_versions (
constructor_id INTEGER,
version_id INTEGER,
UNIQUE (constructor_id, version_id),
PRIMARY KEY (constructor_id, version_id)
FOREIGN KEY (constructor_id)
REFERENCES constructors (constructor_id)
ON DELETE CASCADE
ON UPDATE NO ACTION,
FOREIGN KEY (version_id)
REFERENCES versions (version_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS method_versions (
method_id INTEGER,
version_id INTEGER,
UNIQUE (method_id, version_id),
PRIMARY KEY (method_id, version_id)
FOREIGN KEY (method_id)
REFERENCES methods (method_id)
ON DELETE CASCADE
ON UPDATE NO ACTION,
FOREIGN KEY (version_id)
REFERENCES versions (version_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS member_versions (
member_id INTEGER,
version_id INTEGER,
UNIQUE (member_id, version_id),
PRIMARY KEY (member_id, version_id)
FOREIGN KEY (member_id)
REFERENCES members (member_id)
ON DELETE CASCADE
ON UPDATE NO ACTION,
FOREIGN KEY (version_id)
REFERENCES versions (version_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS constant_versions (
constant_id INTEGER,
version_id INTEGER,
UNIQUE (constant_id, version_id),
PRIMARY KEY (constant_id, version_id)
FOREIGN KEY (constant_id)
REFERENCES constants (constant_id)
ON DELETE CASCADE
ON UPDATE NO ACTION,
FOREIGN KEY (version_id)
REFERENCES versions (version_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS signal_versions (
signal_id INTEGER,
version_id INTEGER,
UNIQUE (signal_id, version_id),
PRIMARY KEY (signal_id, version_id)
FOREIGN KEY (signal_id)
REFERENCES signals (signal_id)
ON DELETE CASCADE
ON UPDATE NO ACTION,
FOREIGN KEY (version_id)
REFERENCES versions (version_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS operator_versions (
operator_id INTEGER,
version_id INTEGER,
UNIQUE (operator_id, version_id),
PRIMARY KEY (operator_id, version_id)
FOREIGN KEY (operator_id)
REFERENCES operators (operator_id)
ON DELETE CASCADE
ON UPDATE NO ACTION,
FOREIGN KEY (version_id)
REFERENCES versions (version_id)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
""")
def insert_entries_and_version(cursor: sqlite3.Cursor, entry_plural: str, entry_singular: str, root: ET.Element, class_id: int, version_id: int):
global db_connection
connection = db_connection
if connection is None:
return
# Methods insert
for entry in root.findall(f"./{entry_plural}/{entry_singular}"):
params = {
"class_id": class_id,
"name": entry.attrib["name"]
}
result = cursor.execute(f"""
INSERT INTO {entry_plural} (class_id, name)
SELECT :class_id, :name
WHERE NOT EXISTS(
SELECT 1 FROM {entry_plural}
WHERE
class_id = :class_id
AND name = :name
)
""", params)
connection.commit()
# Methods version insert
for method in root.findall(f"./{entry_plural}/{entry_singular}"):
params = {
"class_id": class_id,
"name": method.attrib["name"]
}
result = cursor.execute(f"""
SELECT {entry_singular}_id FROM {entry_plural}
WHERE
class_id = :class_id
AND name = :name
""", params)
fetch = result.fetchone()
entry_id = fetch[0]
result = cursor.execute(f"""
INSERT INTO {entry_singular}_versions ({entry_singular}_id, version_id)
SELECT :{entry_singular}_id, :version_id
WHERE NOT EXISTS(
SELECT 1 FROM {entry_singular}_versions
WHERE
{entry_singular}_id = :{entry_singular}_id
AND version_id = :version_id
)
""", {
f"{entry_singular}_id": entry_id,
"version_id": version_id
})
connection.commit()
def parse(repo_path: pathlib.Path, version: semver.Version):
connection = connect_to_db()
cursor = connection.cursor()
if not is_tool("git"):
print_error("git not found")
return
if version.patch:
branch_name = f"{version.major}.{version.minor}.{version.patch}-stable"
else:
branch_name = f"{version.major}.{version.minor}-stable"
process_result = subprocess.run(["git", "checkout", branch_name], cwd=repo_path)
if process_result.returncode > 0:
print_error("Could not checkout on the repo")
return
version_params = {
"major": version.major,
"minor": version.minor,
"patch": version.patch
}
result = cursor.execute("""
SELECT version_id FROM versions
WHERE
major = :major
AND minor = :minor
AND patch = :patch
""", version_params)
fetch = result.fetchone()
if fetch is None:
result = cursor.execute("""
INSERT INTO versions VALUES (null, :major, :minor, :patch)
""", version_params)
connection.commit()
result = cursor.execute("""
SELECT version_id FROM versions
WHERE
major = :major
AND minor = :minor
AND patch = :patch
""", version_params)
fetch = result.fetchone()
version_id = fetch[0]
# Class insert
classes_pathlist = sorted(repo_path.glob("doc/classes/*.xml"))
for path in classes_pathlist:
tree = ET.parse(path)
root = tree.getroot()
class_params = {
"name": root.attrib["name"],
"path": str(path.relative_to(repo_path))
}
result = cursor.execute("""
INSERT INTO classes (name, path)
SELECT :name, :path
WHERE NOT EXISTS(
SELECT 1 FROM classes
WHERE
name = :name
AND path = :path
)
""", class_params)
connection.commit()
# Class loop
classes_pathlist = sorted(repo_path.glob("doc/classes/*.xml"))
for path in classes_pathlist:
print(f"=> {str(path)}")
tree = ET.parse(path)
root = tree.getroot()
class_params = {
"name": root.attrib["name"],
"path": str(path.relative_to(repo_path))
}
result = cursor.execute("""
SELECT class_id FROM classes
WHERE
path = :path
AND name = :name
""", class_params)
fetch = result.fetchone()
class_id = fetch[0]
# Class version insert
class_version_params = {
"class_id": class_id,
"version_id": version_id
}
result = cursor.execute("""
INSERT INTO class_versions (class_id, version_id)
SELECT :class_id, :version_id
WHERE NOT EXISTS(
SELECT 1 FROM class_versions
WHERE
class_id = :class_id
AND version_id = :version_id
)
""", class_version_params)
connection.commit()
# Constructors insert
for constructor in root.findall("./constructors/constructor"):
params = []
for param in constructor.findall("./param"):
params.append((param.attrib["name"], param.attrib["type"]))
name = f"{constructor.attrib["name"]}({", ".join([f"{x[0]}: {x[1]}" for x in params])})"
constructor_params = {
"class_id": class_id,
"name": name
}
result = cursor.execute("""
INSERT INTO constructors (class_id, name)
SELECT :class_id, :name
WHERE NOT EXISTS(
SELECT 1 FROM constructors
WHERE
class_id = :class_id
AND name = :name
)
""", constructor_params)
connection.commit()
# Constructors version insert
for constructor in root.findall("./constructors/constructor"):
params = []
for param in constructor.findall("./param"):
params.append((param.attrib["name"], param.attrib["type"]))
name = f"{constructor.attrib["name"]}({", ".join([f"{x[0]}: {x[1]}" for x in params])})"
constructor_params = {
"class_id": class_id,
"name": name
}
result = cursor.execute("""
SELECT constructor_id FROM constructors
WHERE
class_id = :class_id
AND name = :name
""", constructor_params)
fetch = result.fetchone()
constructor_id = fetch[0]
result = cursor.execute("""
INSERT INTO constructor_versions (constructor_id, version_id)
SELECT :constructor_id, :version_id
WHERE NOT EXISTS(
SELECT 1 FROM constructor_versions
WHERE
constructor_id = :constructor_id
AND version_id = :version_id
)
""", {
"constructor_id": constructor_id,
"version_id": version_id
})
connection.commit()
insert_entries_and_version(cursor, "methods", "method", root, class_id, version_id)
insert_entries_and_version(cursor, "members", "member", root, class_id, version_id)
insert_entries_and_version(cursor, "constants", "constant", root, class_id, version_id)
insert_entries_and_version(cursor, "signals", "signal", root, class_id, version_id)
insert_entries_and_version(cursor, "operators", "operator", root, class_id, version_id)
process_result = subprocess.run(["git", "checkout", "-"], cwd=repo_path)
if process_result.returncode > 0:
print_error("Could not return to previous branch")
return
def parse_version(version_string: str) -> semver.Version:
return semver.Version.parse(version_string, optional_minor_and_patch=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog="godot-api-intro",
description="Checks the introduction version of features in Godot repositories")
subparsers = parser.add_subparsers(dest="subcommand", help="subcommand help")
init_parser = subparsers.add_parser("init", help="Inits the version database.")
init_parser.add_argument("--force", dest="init_force", action="store_true")
parse_parser = subparsers.add_parser("parse", help="Parses a version documentation to detect changes.")
parse_parser.add_argument("repo", type=pathlib.Path)
parse_parser.add_argument("version", type=parse_version)
args = parser.parse_args()
if hasattr(args, "subcommand"):
if args.subcommand == "init":
init_db(args.init_force)
elif args.subcommand == "parse":
parse(args.repo, args.version)
cffi==1.17.1
pycparser==2.22
semver==3.0.4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment