synchronization of developers in entries with developers list

This commit is contained in:
Trilarion 2020-09-06 10:52:44 +02:00
parent cd67ffe536
commit 6b2edf8f56
9 changed files with 390 additions and 256 deletions

View File

@ -119,7 +119,7 @@ def create_toc(title, file, entries):
# assemble rows # assemble rows
rows = [] rows = []
for entry in entries: for entry in entries:
rows.append('- **[{}]({})** ({})'.format(entry['name'], '../' + entry['file'], ', '.join( rows.append('- **[{}]({})** ({})'.format(entry['Name'], '../' + entry['file'], ', '.join(
entry['code language'] + entry['code license'] + entry['state']))) entry['code language'] + entry['code license'] + entry['state'])))
# sort rows (by title) # sort rows (by title)
@ -407,7 +407,7 @@ def update_statistics(infos):
rel(number_inactive)) rel(number_inactive))
if number_inactive > 0: if number_inactive > 0:
entries_inactive = [(x['name'], x['inactive']) for x in infos if 'inactive' in x] entries_inactive = [(x['Name'], x['inactive']) for x in infos if 'inactive' in x]
entries_inactive.sort(key=lambda x: str.casefold(x[0])) # first sort by name entries_inactive.sort(key=lambda x: str.casefold(x[0])) # first sort by name
entries_inactive.sort(key=lambda x: x[1], reverse=True) # then sort by inactive year (more recently first) entries_inactive.sort(key=lambda x: x[1], reverse=True) # then sort by inactive year (more recently first)
entries_inactive = ['{} ({})'.format(*x) for x in entries_inactive] entries_inactive = ['{} ({})'.format(*x) for x in entries_inactive]
@ -422,7 +422,7 @@ def update_statistics(infos):
# number_no_language = sum(1 for x in infois if field not in x) # number_no_language = sum(1 for x in infois if field not in x)
# if number_no_language > 0: # if number_no_language > 0:
# statistics += 'Without language tag: {} ({:.1f}%)\n\n'.format(number_no_language, rel(number_no_language)) # statistics += 'Without language tag: {} ({:.1f}%)\n\n'.format(number_no_language, rel(number_no_language))
# entries_no_language = [x['name'] for x in infois if field not in x] # entries_no_language = [x['Name'] for x in infois if field not in x]
# entries_no_language.sort() # entries_no_language.sort()
# statistics += ', '.join(entries_no_language) + '\n\n' # statistics += ', '.join(entries_no_language) + '\n\n'
@ -447,7 +447,7 @@ def update_statistics(infos):
number_no_license = sum(1 for x in infos if field not in x) number_no_license = sum(1 for x in infos if field not in x)
if number_no_license > 0: if number_no_license > 0:
statistics += 'Without license tag: {} ({:.1f}%)\n\n'.format(number_no_license, rel(number_no_license)) statistics += 'Without license tag: {} ({:.1f}%)\n\n'.format(number_no_license, rel(number_no_license))
entries_no_license = [x['name'] for x in infos if field not in x] entries_no_license = [x['Name'] for x in infos if field not in x]
entries_no_license.sort() entries_no_license.sort()
statistics += ', '.join(entries_no_license) + '\n\n' statistics += ', '.join(entries_no_license) + '\n\n'
@ -491,7 +491,7 @@ def update_statistics(infos):
entries = [] entries = []
for info in infos: for info in infos:
if 'download' not in info and 'play' not in info: if 'download' not in info and 'play' not in info:
entries.append(info['name']) entries.append(info['Name'])
entries.sort(key=str.casefold) entries.sort(key=str.casefold)
statistics += '{}: '.format(len(entries)) + ', '.join(entries) + '\n\n' statistics += '{}: '.format(len(entries)) + ', '.join(entries) + '\n\n'
@ -511,7 +511,7 @@ def update_statistics(infos):
break break
# if there were repositories, but none popular, add them to the list # if there were repositories, but none popular, add them to the list
if not popular: if not popular:
entries.append(info['name']) entries.append(info['Name'])
# print(info[field]) # print(info[field])
entries.sort(key=str.casefold) entries.sort(key=str.casefold)
statistics += '{}: '.format(len(entries)) + ', '.join(entries) + '\n\n' statistics += '{}: '.format(len(entries)) + ', '.join(entries) + '\n\n'
@ -562,7 +562,7 @@ def update_statistics(infos):
c_cpp_project_without_build_system = [] c_cpp_project_without_build_system = []
for info in infos: for info in infos:
if field not in info and ('C' in info['code language'] or 'C++' in info['code language']): if field not in info and ('C' in info['code language'] or 'C++' in info['code language']):
c_cpp_project_without_build_system.append(info['name']) c_cpp_project_without_build_system.append(info['Name'])
c_cpp_project_without_build_system.sort(key=str.casefold) c_cpp_project_without_build_system.sort(key=str.casefold)
statistics += '##### C and C++ projects without build system information ({})\n\n'.format( statistics += '##### C and C++ projects without build system information ({})\n\n'.format(
len(c_cpp_project_without_build_system)) + ', '.join(c_cpp_project_without_build_system) + '\n\n' len(c_cpp_project_without_build_system)) + ', '.join(c_cpp_project_without_build_system) + '\n\n'
@ -572,7 +572,7 @@ def update_statistics(infos):
for info in infos: for info in infos:
if field in info and 'CMake' in info[field] and ( if field in info and 'CMake' in info[field] and (
'C' in info['code language'] or 'C++' in info['code language']): 'C' in info['code language'] or 'C++' in info['code language']):
c_cpp_project_not_cmake.append(info['name']) c_cpp_project_not_cmake.append(info['Name'])
c_cpp_project_not_cmake.sort(key=str.casefold) c_cpp_project_not_cmake.sort(key=str.casefold)
statistics += '##### C and C++ projects with a build system different from CMake ({})\n\n'.format( statistics += '##### C and C++ projects with a build system different from CMake ({})\n\n'.format(
len(c_cpp_project_not_cmake)) + ', '.join(c_cpp_project_not_cmake) + '\n\n' len(c_cpp_project_not_cmake)) + ', '.join(c_cpp_project_not_cmake) + '\n\n'
@ -615,7 +615,7 @@ def export_json(infos):
for info in infos: for info in infos:
# game & description # game & description
entry = ['{} (<a href="{}">home</a>, <a href="{}">entry</a>)'.format(info['name'], info['home'][0], entry = ['{} (<a href="{}">home</a>, <a href="{}">entry</a>)'.format(info['Name'], info['home'][0],
r'https://github.com/Trilarion/opensourcegames/blob/master/entries/' + r'https://github.com/Trilarion/opensourcegames/blob/master/entries/' +
info['file']), info['file']),
textwrap.shorten(info['description'], width=60, placeholder='..')] textwrap.shorten(info['description'], width=60, placeholder='..')]
@ -773,10 +773,10 @@ def export_primary_code_repositories_json(infos):
continue continue
if not consumed: if not consumed:
unconsumed_entries.append([info['name'], info[field]]) unconsumed_entries.append([info['Name'], info[field]])
# print output # print output
if 'code repository' in info: if 'code repository' in info:
print('Entry "{}" unconsumed repo: {}'.format(info['name'], info[field])) print('Entry "{}" unconsumed repo: {}'.format(info['Name'], info[field]))
# sort them alphabetically (and remove duplicates) # sort them alphabetically (and remove duplicates)
for k, v in primary_repos.items(): for k, v in primary_repos.items():
@ -889,7 +889,7 @@ def update_inspirations(infos):
# collect information # collect information
originals = {} originals = {}
for info in infos: for info in infos:
name = info['name'] name = info['Name']
keywords = info['keywords'] keywords = info['keywords']
ins = [x[12:] for x in keywords if x.startswith('inspired by ')] ins = [x[12:] for x in keywords if x.startswith('inspired by ')]
if ins: if ins:
@ -924,7 +924,7 @@ def update_developer(infos):
developer = {} developer = {}
for info in infos: for info in infos:
if 'developer' in info: if 'developer' in info:
name = info['name'] name = info['Name']
devs = info['developer'] devs = info['developer']
for dev in devs: for dev in devs:
if dev in developer: if dev in developer:
@ -954,7 +954,7 @@ def check_code_dependencies(infos):
valid_dependencies = list(utils.constants.general_code_dependencies_without_entry.keys()) valid_dependencies = list(utils.constants.general_code_dependencies_without_entry.keys())
for info in infos: for info in infos:
if any((x in ('framework', 'library', 'game engine') for x in info['keywords'])): if any((x in ('framework', 'library', 'game engine') for x in info['keywords'])):
name = info['name'] name = info['Name']
if name in utils.constants.code_dependencies_aliases: if name in utils.constants.code_dependencies_aliases:
valid_dependencies.extend(utils.constants.code_dependencies_aliases[name]) valid_dependencies.extend(utils.constants.code_dependencies_aliases[name])
else: else:

View File

@ -6,13 +6,14 @@ stored Git repositories.
import os import os
import sys import sys
import requests import requests
from utils import osg, osg_ui
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from utils import constants as c, utils, osg, osg_github from utils import constants as c, utils, osg, osg_github
def developer_info_lookup(name): def developer_info_lookup(name):
for dev in developer_info: for dev in developer_info:
if name == dev['name']: if name == dev['Name']:
return dev return dev
return None return None
@ -21,22 +22,7 @@ def developer_info_lookup(name):
SF_alias_list = {'Erik Johansson (aka feneur)': 'Erik Johansson', 'Itms': 'Nicolas Auvray', SF_alias_list = {'Erik Johansson (aka feneur)': 'Erik Johansson', 'Itms': 'Nicolas Auvray',
'Wraitii': 'Lancelot de Ferrière', 'Simzer': 'Simon Laszlo', 'armin bajramovic': 'Armin Bajramovic'} 'Wraitii': 'Lancelot de Ferrière', 'Simzer': 'Simon Laszlo', 'armin bajramovic': 'Armin Bajramovic'}
if __name__ == "__main__": def test():
# read developer info
developer_info = osg.read_developer_info()
osg.write_developer_info(developer_info) # write again just to make it nice and as sanity check
sys.exit(0)
# assemble info
entries = osg.assemble_infos()
# cross-check
osg.compare_entries_developers(entries, developer_info)
# loop over infos # loop over infos
developers = '' developers = ''
try: try:
@ -44,7 +30,7 @@ if __name__ == "__main__":
# active = False # active = False
for entry in entries: for entry in entries:
# if entry['name'] == 'Aleph One': # if entry['Name'] == 'Aleph One':
# active = True # active = True
# if not active: # if not active:
# continue # continue
@ -55,7 +41,7 @@ if __name__ == "__main__":
break break
# print # print
entry_name = '{} - {}'.format(entry['file'], entry['name']) entry_name = '{} - {}'.format(entry['file'], entry['Name'])
print(entry_name) print(entry_name)
content = '' content = ''
@ -136,3 +122,122 @@ if __name__ == "__main__":
finally: finally:
# store developer info # store developer info
utils.write_text(os.path.join(c.root_path, 'collected_developer_info.txt'), developers) utils.write_text(os.path.join(c.root_path, 'collected_developer_info.txt'), developers)
def compare_entries_developers(entries, developers):
"""
Cross checks the game entries lists and the developers lists.
:param entries: List of game entries
:param developers: List of developers
"""
# from the entries create a dictionary with developer names
devs1 = {}
for entry in entries:
name = entry['Name']
for dev in entry.get('developer', []):
if dev in devs1:
devs1[dev].append(name)
else:
devs1[dev] = [name]
devs1_names = set(devs1.keys())
# from the developers create a dictionary with developer names
devs2 = dict(zip((dev['Name'] for dev in developers), (dev['Games'] for dev in developers)))
devs2_names = set(devs2.keys())
# devs only in entries
for dev in devs1_names - devs2_names:
print('Warning: dev "{}" only in entries ({}), not in developers'.format(dev, ','.join(devs1[dev])))
# devs only in developers
for dev in devs2_names - devs1_names:
print('Warning: dev "{}" only in developers ({}), not in entries'.format(dev, ','.join(devs2[dev])))
# for those in both, check that the games lists are equal
for dev in devs1_names.intersection(devs2_names):
games1 = set(devs1[dev])
games2 = set(devs2[dev])
delta = games1 - games2
if delta:
print('Warning: dev "{}" has games in entries ({}) that are not present in developers'.format(dev,
', '.join(
delta)))
delta = games2 - games1
if delta:
print('Warning: dev "{}" has games in developers ({}) that are not present in entries'.format(dev, delta))
class DevelopersMaintainer:
def __init__(self):
self.developers = None
self.entries = None
def read_developer(self):
self.developers = osg.read_developers()
print('{} developers read'.format(len(self.developers)))
def write_developer(self):
if not self.developers:
print('developers not yet loaded')
return
osg.write_developers(self.developers)
print('developers written')
def check_for_duplicates(self):
if not self.developers:
print('developers not yet loaded')
return
developer_names = [x['Name'] for x in self.developers]
for index, name in enumerate(developer_names):
for other_name in developer_names[index + 1:]:
if osg.name_similarity(name, other_name) > 0.8:
print(' {} - {} is similar'.format(name, other_name))
print('duplicates checked')
def check_for_orphans(self):
if not self.developers:
print('developers not yet loaded')
return
for dev in self.developers:
if not dev['Games']:
print(' {} has no "Games" field'.format(dev['Name']))
print('orphanes checked')
def check_for_missing_developers_in_entries(self):
if not self.developers:
print('developer not yet loaded')
return
if not self.entries:
print('entries not yet loaded')
return
for dev in self.developers:
dev_name = dev['Name']
for entry_name in dev['Games']:
x = [x for x in self.entries if x['Title'] == entry_name]
assert len(x) <= 1
if not x:
print('Entry "{}" listed as game of developer "{}" but this entry does not exist'.format(entry_name, dev_name))
else:
entry = x[0]
if 'Developer' not in entry or dev_name not in entry['Developer']:
print('Entry "{}" listed in developer "{}" but not listed in that entry'.format(entry_name, dev_name))
print('missed developer checked')
def read_entries(self):
self.entries = osg.read_entries()
print('{} entries read'.format(len(self.entries)))
if __name__ == "__main__":
m = DevelopersMaintainer()
actions = {
'Read developers': m.read_developer,
'Write developers': m.write_developer,
'Check for duplicates': m.check_for_duplicates,
'Check for orphans': m.check_for_orphans,
'Check for games in developers not listed': m.check_for_missing_developers_in_entries,
'Read entries': m.read_entries
}
osg_ui.run_simple_button_app('Maintenance developer', actions)

View File

@ -43,8 +43,8 @@ class InspirationMaintainer:
print('inspirations not yet loaded') print('inspirations not yet loaded')
return return
for inspiration in self.inspirations.values(): for inspiration in self.inspirations.values():
if not inspiration['inspired entries']: if not inspiration['Inspired entries']:
print(' {} has no inspired entries'.format(inspiration['name'])) print(' {} has no inspired entries'.format(inspiration['Name']))
print('orphanes checked') print('orphanes checked')
def check_for_missing_inspirations_in_entries(self): def check_for_missing_inspirations_in_entries(self):
@ -55,18 +55,27 @@ class InspirationMaintainer:
print('entries not yet loaded') print('entries not yet loaded')
return return
for inspiration in self.inspirations.values(): for inspiration in self.inspirations.values():
inspiration_name = inspiration['name'] inspiration_name = inspiration['Name']
for entry_name in inspiration['inspired entries']: for entry_name in inspiration['Inspired entries']:
x = [x for x in self.entries if x['title'] == entry_name] x = [x for x in self.entries if x['Title'] == entry_name]
assert len(x) <= 1 assert len(x) <= 1
if not x: if not x:
print('Entry "{}" listed in inspiration "{}" but this entry does not exist'.format(entry_name, inspiration_name)) print('Entry "{}" listed in inspiration "{}" but this entry does not exist'.format(entry_name, inspiration_name))
else: else:
entry = x[0] entry = x[0]
if 'inspirations' not in entry or inspiration_name not in entry['inspirations']: if 'Inspirations' not in entry or inspiration_name not in entry['Inspirations']:
print('Entry "{}" listed in inspiration "{}" but not listed in this entry'.format(entry_name, inspiration_name)) print('Entry "{}" listed in inspiration "{}" but not listed in this entry'.format(entry_name, inspiration_name))
print('missed inspirations checked') print('missed inspirations checked')
def check_for_wikipedia_links(self):
if not self.inspirations:
print('inspirations not yet loaded')
return
for inspiration in self.inspirations.values():
if 'Media' in inspiration and any(('https://en.wikipedia.org/wiki/' in x for x in inspiration['Media'])):
continue
# search in wikipedia
def update_inspired_entries(self): def update_inspired_entries(self):
if not self.inspirations: if not self.inspirations:
print('inspirations not yet loaded') print('inspirations not yet loaded')
@ -76,15 +85,15 @@ class InspirationMaintainer:
return return
# loop over all inspirations and delete inspired entries # loop over all inspirations and delete inspired entries
for inspiration in self.inspirations.values(): for inspiration in self.inspirations.values():
inspiration['inspired entries'] = [] inspiration['Inspired entries'] = []
# loop over all entries and add to inspirations of entry # loop over all entries and add to inspirations of entry
for entry in self.entries: for entry in self.entries:
entry_name = entry['title'] entry_name = entry['Title']
for inspiration in entry.get('inspirations', []): for inspiration in entry.get('Inspirations', []):
if inspiration in self.inspirations: if inspiration in self.inspirations:
self.inspirations[inspiration]['inspired entries'].append(entry_name) self.inspirations[inspiration]['Inspired entries'].append(entry_name)
else: else:
self.inspirations[inspiration] = {'name': inspiration, 'inspired entries': [entry_name]} self.inspirations[inspiration] = {'Name': inspiration, 'Inspired entries': [entry_name]}
print('inspired entries updated') print('inspired entries updated')
def read_entries(self): def read_entries(self):

View File

@ -12,7 +12,7 @@ tocs_path = os.path.join(entries_path, 'tocs')
code_path = os.path.join(root_path, 'code') code_path = os.path.join(root_path, 'code')
inspirations_file = os.path.join(root_path, 'inspirations.md') inspirations_file = os.path.join(root_path, 'inspirations.md')
developer_file = os.path.join(root_path, 'developer.md') developer_file = os.path.join(root_path, 'developers.md')
# local config # local config
local_config_file = os.path.join(root_path, 'local-config.ini') local_config_file = os.path.join(root_path, 'local-config.ini')
@ -33,12 +33,12 @@ def get_config(key):
generic_comment_string = '[comment]: # (partly autogenerated content, edit with care, read the manual before)' generic_comment_string = '[comment]: # (partly autogenerated content, edit with care, read the manual before)'
# these fields have to be present in each entry (in this order) # these fields have to be present in each entry (in this order)
essential_fields = ('Home', 'State', 'Keywords', 'Code repository', 'Code language', 'Code license') essential_fields = ('Title', 'Home', 'State', 'Keywords', 'Code repository', 'Code language', 'Code license')
# only these fields can be used currently (in this order) # only these fields can be used currently (in this order)
valid_fields = ( valid_fields = (
'Home', 'Media', 'Inspirations', 'State', 'Play', 'Download', 'Platform', 'Keywords', 'Code repository', 'Code language', 'Title', 'Home', 'Media', 'Inspirations', 'State', 'Play', 'Download', 'Platform', 'Keywords', 'Code repository', 'Code language',
'Code license', 'Code dependencies', 'Assets license', 'Developer') 'Code license', 'Code dependencies', 'Assets license', 'Developer', 'Note', 'Building')
valid_building_fields = ('Build system', 'Build instructions') valid_building_fields = ('Build system', 'Build instructions')
@ -91,7 +91,9 @@ general_code_dependencies_without_entry = {'OpenGL': 'https://www.opengl.org/',
'tkinter': 'https://docs.python.org/3/library/tk.html'} 'tkinter': 'https://docs.python.org/3/library/tk.html'}
# developer information (in the file all fields will be capitalized) # developer information (in the file all fields will be capitalized)
valid_developer_fields = ('name', 'games', 'contact', 'organization', 'home') essential_developer_fields = ('Name', 'Games')
valid_developer_fields = essential_developer_fields + ('Contact', 'Home', 'Organization')
# inspiration/original game information (in the file all fields will be capitalized) # inspiration/original game information (in the file all fields will be capitalized)
valid_inspiration_fields = ('name', 'inspired entries', 'media') essential_inspiration_fields = ('Name', 'Inspired entries')
valid_inspiration_fields = essential_inspiration_fields + ('Media',)

View File

@ -4,104 +4,9 @@ Specific functions working on the games.
import re import re
from difflib import SequenceMatcher from difflib import SequenceMatcher
from utils import utils from utils import utils, osg_parse
import lark
from utils.constants import * from utils.constants import *
class ListingTransformer(lark.Transformer):
"""
Transforms content parsed by grammar_listing.lark further.
Used for the developer and inspirations list.
"""
def unquoted_value(self, x):
return x[0].value
def quoted_value(self, x):
return x[0].value[1:-1] # remove quotation marks
def property(self, x):
"""
The key of a property will be converted to lower case and the value part is the second part
:param x:
:return:
"""
return x[0].lower(), x[1:]
def name(self, x):
"""
The name part is treated as a property with key "name"
:param x:
:return:
"""
return 'name', x[0].value
def entry(self, x):
"""
All (key, value) tuples are inserted into a dictionary.
:param x:
:return:
"""
d = {}
for key, value in x:
if key in d:
raise RuntimeError('Key in entry appears twice')
d[key] = value
return d
def start(self, x):
return x
# transformer
class EntryTransformer(lark.Transformer):
def unquoted_value(self, x):
return x[0].value
def quoted_value(self, x):
return x[0].value[1:-1] # remove quotation marks
def property(self, x):
"""
The key of a property will be converted to lower case and the value part is the second part
:param x:
:return:
"""
return x[0].lower(), x[1:]
def title(self, x):
return 'title', x[0].value
def note(self, x):
"""
Optional
:param x:
:return:
"""
if not x:
raise lark.Discard
return 'note', ''.join((x.value for x in x))
def building(self, x):
d = {}
for key, value in x:
if key in d:
raise RuntimeError('Key in entry appears twice')
d[key] = value
return 'building', d
def start(self, x):
d = {}
for key, value in x:
if key in d:
raise RuntimeError('Key in entry appears twice')
d[key] = value
return d
regex_sanitize_name = re.compile(r"[^A-Za-z 0-9-+]+") regex_sanitize_name = re.compile(r"[^A-Za-z 0-9-+]+")
regex_sanitize_name_space_eater = re.compile(r" +") regex_sanitize_name_space_eater = re.compile(r" +")
@ -387,48 +292,32 @@ def extract_links():
return urls return urls
def read_and_parse(content_file: str, grammar_file: str, transformer: lark.Transformer): def read_developers():
"""
Reads a content file and a grammar file and parses the content with the grammar following by
transforming the parsed output and returning the transformed result.
:param content_file:
:param grammar_file:
:param transformer:
:return:
"""
content = utils.read_text(content_file)
grammar = utils.read_text(grammar_file)
parser = lark.Lark(grammar, debug=False, parser='lalr')
tree = parser.parse(content)
return transformer.transform(tree)
def read_developer_info():
""" """
:return: :return:
""" """
grammar_file = os.path.join(code_path, 'grammar_listing.lark') grammar_file = os.path.join(code_path, 'grammar_listing.lark')
transformer = ListingTransformer() developers = osg_parse.read_and_parse(developer_file, grammar_file, osg_parse.ListingTransformer)
developers = read_and_parse(developer_file, grammar_file, transformer)
# now transform a bit more # now transform a bit more
for index, dev in enumerate(developers): for index, dev in enumerate(developers):
# check for valid keys # check for valid keys
for field in dev.keys(): for field in dev.keys():
if field not in valid_developer_fields: if field not in valid_developer_fields:
raise RuntimeError('Unknown developer field "{}" for developer: {}.'.format(field, dev['name'])) raise RuntimeError('Unknown developer field "{}" for developer: {}.'.format(field, dev['Name']))
# strip from name or organization (just in case) # strip from name or organization (just in case)
for field in ('name', ): for field in ('Name', ):
if field in dev: if field in dev:
dev[field] = dev[field].strip() dev[field] = dev[field].strip()
# split games, contact (are lists) # split games, contact (are lists)
for field in ('games', 'contact'): for field in ('Games', 'Contact'):
if field in dev: if field in dev:
content = dev[field] content = dev[field]
content = [x.strip() for x in content] content = [x.strip() for x in content]
dev[field] = content dev[field] = content
# check for duplicate names entries # check for duplicate names entries
names = [dev['name'] for dev in developers] names = [dev['Name'] for dev in developers]
duplicate_names = (name for name in names if names.count(name) > 1) duplicate_names = (name for name in names if names.count(name) > 1)
duplicate_names = set(duplicate_names) # to avoid duplicates in duplicate_names duplicate_names = set(duplicate_names) # to avoid duplicates in duplicate_names
if duplicate_names: if duplicate_names:
@ -436,7 +325,7 @@ def read_developer_info():
return developers return developers
def write_developer_info(developers): def write_developers(developers):
""" """
:return: :return:
@ -448,19 +337,19 @@ def write_developer_info(developers):
content += '# Developer [{}]\n\n'.format(len(developers)) content += '# Developer [{}]\n\n'.format(len(developers))
# sort by name # sort by name
developers.sort(key=lambda x: str.casefold(x['name'])) developers.sort(key=lambda x: str.casefold(x['Name']))
# iterate over them # iterate over them
for dev in developers: for dev in developers:
keys = list(dev.keys()) keys = list(dev.keys())
# developer name # developer name
content += '## {} [{}]\n\n'.format(dev['name'], len(dev['games'])) content += '## {} [{}]\n\n'.format(dev['Name'], len(dev['games']))
keys.remove('name') keys.remove('Name')
# all the remaining in alphabetical order, but 'games' first # all the remaining in alphabetical order, but 'games' first
keys.remove('games') keys.remove('Games')
keys.sort() keys.sort()
keys = ['games'] + keys keys = ['Games'] + keys
for field in keys: for field in keys:
value = dev[field] value = dev[field]
field = field.capitalize() field = field.capitalize()
@ -484,34 +373,34 @@ def read_inspirations():
""" """
# read inspirations # read inspirations
# read and parse inspirations
grammar_file = os.path.join(code_path, 'grammar_listing.lark') grammar_file = os.path.join(code_path, 'grammar_listing.lark')
transformer = ListingTransformer() inspirations = osg_parse.read_and_parse(inspirations_file, grammar_file, osg_parse.ListingTransformer)
inspirations = read_and_parse(inspirations_file, grammar_file, transformer)
# now inspirations is a list of dictionaries for every entry with keys (valid_developers_fields) # now inspirations is a list of dictionaries for every entry with keys (valid_developers_fields)
# now transform a bit more # now transform a bit more
for index, inspiration in enumerate(inspirations): for inspiration in inspirations:
# check that keys are valid keys # check that keys are valid keys
for field in inspiration.keys(): for field in inspiration.keys():
if field not in valid_inspiration_fields: if field not in valid_inspiration_fields:
raise RuntimeError('Unknown field "{}" for inspiration: {}.'.format(field, inspiration['name'])) raise RuntimeError('Unknown field "{}" for inspiration: {}.'.format(field, inspiration['Name']))
# split lists # split lists
for field in ('inspired entries',): for field in ('Inspired entries',):
if field in inspiration: if field in inspiration:
content = inspiration[field] content = inspiration[field]
content = [x.strip() for x in content] content = [x.strip() for x in content]
inspiration[field] = content inspiration[field] = content
# check for duplicate names entries # check for duplicate names entries
names = [inspiration['name'] for inspiration in inspirations] names = [inspiration['Name'] for inspiration in inspirations]
duplicate_names = (name for name in names if names.count(name) > 1) duplicate_names = (name for name in names if names.count(name) > 1)
duplicate_names = set(duplicate_names) # to avoid duplicates in duplicate_names duplicate_names = set(duplicate_names) # to avoid duplicates in duplicate_names
if duplicate_names: if duplicate_names:
raise RuntimeError('Duplicate inspiration names: {}'.format(', '.join(duplicate_names))) raise RuntimeError('Duplicate inspiration names: {}'.format(', '.join(duplicate_names)))
# convert to dictionary # convert to dictionary
inspirations = {x['name']: x for x in inspirations} inspirations = {x['Name']: x for x in inspirations}
return inspirations return inspirations
@ -532,25 +421,25 @@ def write_inspirations(inspirations):
content += '# Inspirations [{}]\n\n'.format(len(inspirations)) content += '# Inspirations [{}]\n\n'.format(len(inspirations))
# sort by name # sort by name
inspirations.sort(key=lambda x: str.casefold(x['name'])) inspirations.sort(key=lambda x: str.casefold(x['Name']))
# iterate over them # iterate over them
for inspiration in inspirations: for inspiration in inspirations:
keys = list(inspiration.keys()) keys = list(inspiration.keys())
# inspiration name # inspiration name
content += '## {} [{}]\n\n'.format(inspiration['name'], len(inspiration['inspired entries'])) content += '## {} [{}]\n\n'.format(inspiration['Name'], len(inspiration['Inspired entries']))
keys.remove('name') keys.remove('Name')
# all the remaining in alphabetical order, but "inspired entries" first # all the remaining in alphabetical order, but "inspired entries" first
keys.remove('inspired entries') keys.remove('Inspired entries')
keys.sort() keys.sort()
keys = ['inspired entries'] + keys keys = ['Inspired entries'] + keys
for field in keys: for field in keys:
value = inspiration[field] value = inspiration[field]
field = field.capitalize() field = field.capitalize()
# lists get special treatment # lists get special treatment
if isinstance(value, list): if isinstance(value, list):
value.sort(key=str.casefold) # sorted alphabetically value.sort(key=str.casefold) # sorted alphabetically
value = [x if not ',' in x else '"{}"'.format(x) for x in value] # surround those with a comma with quotation marks value = [x if not ',' in x else '"{}"'.format(x) for x in value] # surround those with a comma with quotation marks
value = ', '.join(value) value = ', '.join(value)
content += '- {}: {}\n'.format(field, value) content += '- {}: {}\n'.format(field, value)
@ -565,13 +454,10 @@ def read_entries():
Parses all entries and assembles interesting infos about them. Parses all entries and assembles interesting infos about them.
""" """
# setup parser # setup parser and transformer
grammar_file = os.path.join(code_path, 'grammar_entries.lark') grammar_file = os.path.join(code_path, 'grammar_entries.lark')
grammar = utils.read_text(grammar_file) grammar = utils.read_text(grammar_file)
parser = lark.Lark(grammar, debug=False, parser='lalr') parse = osg_parse.create(grammar, osg_parse.EntryTransformer)
# setup transformer
transformer = EntryTransformer()
# a database of all important infos about the entries # a database of all important infos about the entries
entries = [] entries = []
@ -585,13 +471,13 @@ def read_entries():
# parse and transform entry content # parse and transform entry content
try: try:
tree = parser.parse(content) entry = parse(content)
entry = transformer.transform(tree)
# add file information # add file information
entry['file'] = file entry['File'] = file
check_entry(entry) check_entry(entry)
post_process(entry)
except Exception as e: except Exception as e:
print('{} - {}'.format(file, e)) print('{} - {}'.format(file, e))
exception_happened = True exception_happened = True
@ -604,6 +490,22 @@ def read_entries():
return entries return entries
def post_process(entry):
"""
:param entry:
:return:
"""
# remove all parentheses from developers
if 'Developer' in entry:
devs = entry['Developer']
devs = [re.sub(r'\([^)]*\)', '', x).strip() for x in devs]
if any(not x for x in devs):
raise RuntimeError('Empty developer')
entry['Developer'] = devs
def check_entry(entry): def check_entry(entry):
""" """
@ -613,14 +515,19 @@ def check_entry(entry):
""" """
message = '' message = ''
file = entry['file'] file = entry['File']
# check canonical file name # check canonical file name
canonical_file_name = canonical_entry_name(entry['title']) + '.md' canonical_file_name = canonical_entry_name(entry['Title']) + '.md'
# we also allow -X with X =2..9 as possible extension (because of duplicate canonical file names) # we also allow -X with X =2..9 as possible extension (because of duplicate canonical file names)
if canonical_file_name != file and canonical_file_name != file[:-5] + '.md': if canonical_file_name != file and canonical_file_name != file[:-5] + '.md':
message += 'file name should be {}\n'.format(canonical_file_name) message += 'file name should be {}\n'.format(canonical_file_name)
# check for essential fields
for field in essential_fields:
if field not in entry:
message += 'essential property "{}" missing\n'.format(field)
if message: if message:
raise RuntimeError(message) raise RuntimeError(message)
@ -661,20 +568,19 @@ def create_entry_content(entry):
""" """
# title # title
content = '# {}\n\n'.format(entry['title']) content = '# {}\n\n'.format(entry['Title'])
# now properties in the recommended order # now properties in the recommended order
for field in valid_fields: for field in valid_fields:
field_name = field.lower() if field in entry:
if field_name in entry: c = entry[field]
c = entry[field_name]
c = ['"{}"'.format(x) if ',' in x else x for x in c] c = ['"{}"'.format(x) if ',' in x else x for x in c]
content += '- {}: {}\n'.format(field, ', '.join(c)) content += '- {}: {}\n'.format(field, ', '.join(c))
content += '\n' content += '\n'
# if there is a note, insert it # if there is a note, insert it
if 'note' in entry: if 'Note' in entry:
content += entry['note'] content += entry['Note']
# building header # building header
content += '## Building\n' content += '## Building\n'
@ -682,59 +588,15 @@ def create_entry_content(entry):
# building properties if present # building properties if present
has_properties = False has_properties = False
for field in valid_building_fields: for field in valid_building_fields:
field_name = field.lower() if field in entry['Building']:
if field_name in entry['building']:
if not has_properties: if not has_properties:
has_properties = True has_properties = True
content += '\n' content += '\n'
content += '- {}: {}\n'.format(field, ', '.join(entry['building'][field_name])) content += '- {}: {}\n'.format(field, ', '.join(entry['Building'][field]))
# if there is a note, insert it # if there is a note, insert it
if 'note' in entry['building']: if 'Note' in entry['Building']:
content += '\n' content += '\n'
content += entry['building']['note'] content += entry['Building']['Note']
return content return content
def compare_entries_developers(entries, developers):
"""
Cross checks the game entries lists and the developers lists.
:param entries: List of game entries
:param developers: List of developers
"""
# from the entries create a dictionary with developer names
devs1 = {}
for entry in entries:
name = entry['name']
for dev in entry.get('developer', []):
if dev in devs1:
devs1[dev].append(name)
else:
devs1[dev] = [name]
devs1_names = set(devs1.keys())
# from the developers create a dictionary with developer names
devs2 = dict(zip((dev['name'] for dev in developers), (dev['games'] for dev in developers)))
devs2_names = set(devs2.keys())
# devs only in entries
for dev in devs1_names - devs2_names:
print('Warning: dev "{}" only in entries ({}), not in developers'.format(dev, ','.join(devs1[dev])))
# devs only in developers
for dev in devs2_names - devs1_names:
print('Warning: dev "{}" only in developers ({}), not in entries'.format(dev, ','.join(devs2[dev])))
# for those in both, check that the games lists are equal
for dev in devs1_names.intersection(devs2_names):
games1 = set(devs1[dev])
games2 = set(devs2[dev])
delta = games1 - games2
if delta:
print('Warning: dev "{}" has games in entries ({}) that are not present in developers'.format(dev,
', '.join(
delta)))
delta = games2 - games1
if delta:
print('Warning: dev "{}" has games in developers ({}) that are not present in entries'.format(dev,
', '.join(
delta)))

View File

@ -0,0 +1,140 @@
"""
"""
from functools import partial
import lark
from utils import utils, constants as c
class ListingTransformer(lark.Transformer):
"""
Transforms content parsed by grammar_listing.lark further.
Used for the developer and inspirations list.
"""
def unquoted_value(self, x):
return x[0].value
def quoted_value(self, x):
return x[0].value[1:-1] # remove quotation marks
def property(self, x):
"""
The key of a property will be converted to lower case and the value part is the second part
:param x:
:return:
"""
return x[0], x[1:]
def name(self, x):
"""
The name part is treated as a property with key "Name"
:param x:
:return:
"""
return 'Name', x[0].value
def entry(self, x):
"""
All (key, value) tuples are inserted into a dictionary.
:param x:
:return:
"""
d = {}
for key, value in x:
if key in d:
raise RuntimeError('Key in entry appears twice')
d[key] = value
return d
def start(self, x):
return x
# transformer
class EntryTransformer(lark.Transformer):
def unquoted_value(self, x):
return x[0].value
def quoted_value(self, x):
return x[0].value[1:-1] # remove quotation marks
def property(self, x):
"""
The key of a property will be converted to lower case and the value part is the second part
:param x:
:return:
"""
return x[0], x[1:]
def title(self, x):
return 'Title', x[0].value
def note(self, x):
"""
Optional
:param x:
:return:
"""
if not x:
raise lark.Discard
return 'Note', ''.join((x.value for x in x))
def building(self, x):
d = {}
for key, value in x:
if key in d:
raise RuntimeError('Key in entry appears twice')
d[key] = value
return 'Building', d
def start(self, x):
# we do the essential fields and valid fields checks right here
fields = [x[0] for x in x]
# check for essential fields
for field in c.essential_fields:
if field not in fields:
raise RuntimeError('Essential field "{}" is missing'.format(field))
# check for valid fields (in that order)
index = 0
for field in fields:
while index < len(c.valid_fields) and field != c.valid_fields[index]:
index += 1
if index == len(c.valid_fields):
raise RuntimeError('Field "{}" either not valid or in wrong order'.format(field))
d = {}
for key, value in x:
if key in d:
raise RuntimeError('Key in entry appears twice')
d[key] = value
return d
def parse(parser, transformer, content):
tree = parser.parse(content)
value = transformer.transform(tree)
return value
def create(grammar, Transformer):
parser = lark.Lark(grammar, debug=False, parser='lalr')
transformer = Transformer()
return partial(parse, parser, transformer)
def read_and_parse(content_file: str, grammar_file: str, Transformer: lark.Transformer):
"""
Reads a content file and a grammar file and parses the content with the grammar following by
transforming the parsed output and returning the transformed result.
:param content_file:
:param grammar_file:
:param transformer:
:return:
"""
grammar = utils.read_text(grammar_file)
parse = create(grammar, Transformer)
content = utils.read_text(content_file)
return parse(content)

View File

@ -0,0 +1,16 @@
"""
Everything specific to search in Wikipedia.
Using https://github.com/goldsmith/Wikipedia
"""
import wikipedia
def search(search_term, results=3):
"""
:param search_term:
:param max_results:
:return:
"""
return wikipedia.search(search_term, results=results)

View File

@ -32,7 +32,7 @@
## Alex Margarit [1] ## Alex Margarit [1]
- Games: a2x - Games: faur
## Alexander Lang [1] ## Alexander Lang [1]

View File

@ -10,7 +10,7 @@
- Code dependencies: SDL - Code dependencies: SDL
- Developer: Matthew Sarnoff (Game Creation Society), Chris DeLeon (Game Creation Society), John Nesky (Game Creation Society), Gregory Peng (Game Creation Society), Jeff Thoene (Game Creation Society), Tuscan Knox (music, Game Creation Society), Michael Weber (Game Creation Society) - Developer: Matthew Sarnoff (Game Creation Society), Chris DeLeon (Game Creation Society), John Nesky (Game Creation Society), Gregory Peng (Game Creation Society), Jeff Thoene (Game Creation Society), Tuscan Knox (music, Game Creation Society), Michael Weber (Game Creation Society)
Shooting game which uses a 3d engine but allows 2d gameplay. Shooting game which uses a 3D engine but allows 2D gameplay.
## Building ## Building