maintenance

This commit is contained in:
Trilarion
2019-12-05 17:27:48 +01:00
parent 7021becd32
commit 29355829e6
62 changed files with 426 additions and 375 deletions

View File

@ -12,14 +12,29 @@ valid_fields = ('Home', 'Media', 'State', 'Play', 'Download', 'Platform', 'Keywo
'Code license', 'Code dependencies', 'Assets license', 'Build system', 'Build instructions')
valid_platforms = ('Windows', 'Linux', 'macOS', 'Android', 'iOS', 'Web')
recommended_keywords = ('action', 'arcade', 'adventure', 'visual novel', 'sports', 'platform', 'puzzle', 'role playing', 'simulation', 'strategy', 'card game', 'board game', 'music', 'educational', 'tool', 'game engine', 'framework', 'library', 'remake')
known_languages = ('AGS Script', 'ActionScript', 'Ada', 'AngelScript', 'Assembly', 'Basic', 'Blender Script', 'BlitzMax', 'C', 'C#', 'C++', 'Clojure', 'CoffeeScript', 'ColdFusion', 'D', 'DM', 'Dart', 'Dia', 'Elm', 'Emacs Lisp', 'F#', 'GDScript', 'Game Maker Script', 'Go', 'Groovy', 'Haskell', 'Haxe', 'Io', 'Java', 'JavaScript', 'Kotlin', 'Lisp', 'Lua', 'MegaGlest Script', 'MoonScript', 'None', 'OCaml', 'Objective-C', 'PHP', 'Pascal', 'Perl', 'Python', 'QuakeC', 'R', "Ren'py", 'Ruby', 'Rust', 'Scala', 'Scheme', 'Script', 'Shell', 'Swift', 'TorqueScript', 'TypeScript', 'Vala', 'Visual Basic', 'XUL', 'ZenScript', 'ooc')
known_licenses = ('2-clause BSD', '3-clause BSD', 'AFL-3.0', 'AGPL-3.0', 'Apache-2.0', 'Artistic License-1.0', 'Artistic License-2.0', 'Boost-1.0', 'CC-BY-NC-3.0', 'CC-BY-NC-SA-2.0', 'CC-BY-SA-3.0', 'CC-BY-SA-4.0', 'CC0', 'Custom', 'EPL-2.0', 'GPL-2.0', 'GPL-3.0', 'IJG', 'ISC', 'Java Research License', 'LGPL-2.0', 'LGPL-2.1', 'LGPL-3.0', 'MAME', 'MIT', 'MPL-1.1', 'MPL-2.0', 'MS-PL', 'MS-RL', 'NetHack General Public License', 'None', 'Proprietary', 'Public domain', 'SWIG license', 'Unlicense', 'WTFPL', 'wxWindows license', 'zlib')
known_multiplayer_modes = ('competitive', 'co-op', 'hotseat', 'LAN', 'local', 'massive', 'matchmaking', 'online', 'split-screen')
regex_sanitize_name = re.compile(r"[^A-Za-z 0-9-+]+")
regex_sanitize_name_space_eater = re.compile(r" +")
def game_name_similarity(a, b):
def name_similarity(a, b):
return SequenceMatcher(None, str.casefold(a), str.casefold(b)).ratio()
def split_infos(infos):
"""
Split into games, tools, frameworks, libraries
"""
games = [x for x in infos if not any([y in x['keywords'] for y in ('tool', 'framework', 'library')])]
tools = [x for x in infos if 'tool' in x['keywords']]
frameworks = [x for x in infos if 'framework' in x['keywords']]
libraries = [x for x in infos if 'library' in x['keywords']]
return games, tools, frameworks, libraries
def entry_iterator():
"""
@ -43,7 +58,7 @@ def entry_iterator():
yield entry, entry_path, content
def canonical_game_name(name):
def canonical_entry_name(name):
"""
Derives a canonical game name from an actual game name (suitable for file names, ...)
"""
@ -59,7 +74,9 @@ def canonical_game_name(name):
def parse_entry(content):
"""
Returns a dictionary of the features of the content
Returns a dictionary of the features of the content.
Raises errors when a major error in the structure is expected, prints a warning for minor errors.
"""
info = {}
@ -67,14 +84,14 @@ def parse_entry(content):
# read name
regex = re.compile(r"^# (.*)") # start of content, starting with "# " and then everything until the end of line
matches = regex.findall(content)
if len(matches) != 1 or not matches[0]:
if len(matches) != 1 or not matches[0]: # name must be there
raise RuntimeError('Name not found in entry "{}" : {}'.format(content, matches))
info['name'] = matches[0]
# read description
regex = re.compile(r"^.*\n\n_(.*)_\n") # third line from top, everything between underscores
matches = regex.findall(content)
if len(matches) != 1 or not matches[0]:
if len(matches) != 1 or not matches[0]: # description must be there
raise RuntimeError('Description not found in entry "{}"'.format(content))
info['description'] = matches[0]
@ -84,7 +101,7 @@ def parse_entry(content):
# check that essential fields are there
for field in essential_fields:
if field not in fields:
if field not in fields: # essential fields must be there
raise RuntimeError('Essential field "{}" missing in entry "{}"'.format(field, info['name']))
# check that all fields are valid fields and are existing in that order
@ -92,15 +109,14 @@ def parse_entry(content):
for field in fields:
while index < len(valid_fields) and field != valid_fields[index]:
index += 1
if index == len(valid_fields):
if index == len(valid_fields): # must be valid fields and must be in the right order
raise RuntimeError('Field "{}" in entry "{}" either misspelled or in wrong order'.format(field, info['name']))
# iterate over found fields
for field in fields:
regex = re.compile(r"- {}: (.*)".format(field))
matches = regex.findall(content)
if len(matches) != 1:
# every field should only be present once
if len(matches) != 1: # every field must be present only once
raise RuntimeError('Field "{}" in entry "{}" exist multiple times.'.format(field, info['name']))
v = matches[0]
@ -129,17 +145,17 @@ def parse_entry(content):
# store in info
info[field.lower()] = v
# check that essential fields made it through
# check again that essential fields made it through
for field in ('home', 'state', 'keywords', 'code language', 'code license'):
if field not in info:
raise RuntimeError('Essential field "{}" missing or empty in entry "{}"'.format(field, info['name']))
if field not in info: # essential fields must still be inside
raise RuntimeError('Essential field "{}" empty in entry "{}"'.format(field, info['name']))
# now checks on the content of fields
# name should not have spaces at the begin or end
v = info['name']
if len(v) != len(v.strip()):
raise RuntimeError('No leading or trailing spaces in the entry name, "{}"'.format(info['name']))
if len(v) != len(v.strip()): # warning about that
print('Warning: No leading or trailing spaces in the entry name, "{}"'.format(info['name']))
# state (essential field) must contain either beta or mature but not both, but at least one
v = info['state']
@ -165,11 +181,14 @@ def parse_entry(content):
if ' ' in url:
raise RuntimeError('URL "{}" in entry "{}" contains a space'.format(url, info['name']))
# github repositories should end on .git
# github/gitlab repositories should end on .git and should start with https
if 'code repository' in info:
for repo in info['code repository']:
if repo.startswith('https://github.com/') and not repo.endswith('.git'):
raise RuntimeError('Github repo {} in entry "{}" should end on .git.'.format(repo, info['name']))
if any((x in repo for x in ('github', 'gitlab', 'git.tuxfamily', 'git.savannah'))):
if not repo.startswith('https://'):
print('Warning: Repo {} in entry "{}" should start with https://'.format(repo, info['name']))
if not repo.endswith('.git'):
print('Warning: Repo {} in entry "{}" should end on .git.'.format(repo, info['name']))
# check that all platform tags are valid tags and are existing in that order
if 'platform' in info:
@ -177,7 +196,7 @@ def parse_entry(content):
for platform in info['platform']:
while index < len(valid_platforms) and platform != valid_platforms[index]:
index += 1
if index == len(valid_platforms):
if index == len(valid_platforms): # must be valid platforms and must be in that order
raise RuntimeError('Platform tag "{}" in entry "{}" either misspelled or in wrong order'.format(platform, info['name']))
# there must be at least one keyword
@ -190,9 +209,21 @@ def parse_entry(content):
if recommended_keyword in info['keywords']:
fail = False
break
if fail:
if fail: # must be at least one recommended keyword
raise RuntimeError('Entry "{}" contains no recommended keyword'.format(info['name']))
# languages should be known
languages = info['code language']
for language in languages:
if language not in known_languages:
print('Warning: Language {} in entry "{}" is not a known language. Misspelled or new?'.format(language, info['name']))
# licenses should be known
licenses = info['code license']
for license in licenses:
if license not in known_licenses:
print('Warning: License {} in entry "{}" is not a known license. Misspelled or new?'.format(license, info['name']))
return info
@ -216,7 +247,7 @@ def assemble_infos():
info['file'] = entry
# check canonical file name
canonical_file_name = canonical_game_name(info['name']) + '.md'
canonical_file_name = canonical_entry_name(info['name']) + '.md'
# we also allow -X with X =2..9 as possible extension (because of duplicate canonical file names)
if canonical_file_name != entry and canonical_file_name != entry[:-5] + '.md':
print('file {} should be {}'.format(entry, canonical_file_name))