Siema! Rzuci ktoś okiem na mój programik napisany w Pythonie?
Piszę program do nauki historii. Mam zamiar dokonać w nim kilku ulepszeń jak np. ogarnięcie formatu json zamiast pickle, zrobienie jakiegoś gui w tk lub curses, dodanie ascii artów.
Byłbym wdzięczny za code review.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import date
import pickle
import enum
import os
import random
def read_int(prompt=None, error=None):
if prompt is None:
prompt = 'Enter a number'
if error is None:
error = 'Not a number.'
while True:
try:
return int(input(prompt))
except ValueError:
print(error)
def read_in_range(prompt=None, error=None, lower=None, upper=None):
if prompt is None:
prompt = f'Enter a number in range [{lower}; {upper}): '
if error is None:
error = 'Not a number.'
while True:
try:
user_input = input(prompt)
if not user_input.isdigit():
raise ValueError(error)
user_input = int(user_input)
if lower and user_input < lower:
raise ValueError(f'Too small: number have to be in range [{lower}; {upper}).')
if upper and user_input > upper:
raise ValueError(f'Too high: number have to be in range [{lower}; {upper}).')
return user_input
except ValueError as e:
print(e)
@enum.unique
class Era(enum.Enum):
BCE = enum.auto()
CE = enum.auto()
def read_historical_year(prompt=None, error=None):
if prompt is None:
prompt = f'Enter a year in the format yyyy [{Era.BCE.name}|{Era.CE.name}]: '
if error is None:
error = f'Year must be in the format yyyy [{Era.BCE.name}|{Era.CE.name}].'
while True:
user_input = input(prompt)
user_input = user_input.split(' ')
year = user_input[0]
era = None
if len(user_input) == 2:
era = user_input[-1]
elif len(user_input) == 1:
era = Era.CE.name
if not year.isdigit() or era not in {Era.BCE.name, Era.CE.name}:
print(error)
else:
if era == Era.BCE.name:
return HistoricalDate(int(year), Era.BCE)
else:
return HistoricalDate(int(year), Era.CE)
class Choice:
def __init__(self, name, value):
self.name = name
self.value = value
def __str__(self):
return self.name
def read_choice(choices, header=None, prompt=None, error=None):
if header is None:
header = 'Available choices:'
if prompt is None:
prompt = 'Your choice: '
if error is None:
error = 'Invalid choice!'
while True:
try:
print(header)
for offset, item in enumerate(choices):
print(f'{offset}) {item}')
user_choice = input(prompt)
if user_choice.isdigit():
user_choice = int(user_choice)
else:
raise ValueError('Not a number.')
if user_choice < 0 or user_choice >= len(choices):
raise ValueError(error)
return choices[user_choice].value
except ValueError as e:
print(e)
today = date.today()
class HistoricalDate:
def __init__(self, year=today.year, era=Era.CE):
self.year = year
self.era = era
def __str__(self):
return f'{self.year} {self.era.name}'
def __add__(self, other):
current_year = self.year
if ((self.era == Era.CE and other.era == Era.CE)
or (self.era == Era.BCE and other.era == Era.BCE)):
current_year += other.year
elif self.era == Era.CE and other.era == Era.BCE:
current_year -= other.year
if current_year <= 0:
current_year -= 1
return HistoricalDate(-current_year, Era.BCE)
else:
current_year -= other.year
if current_year <= 0:
current_year -= 1
return HistoricalDate(-current_year, Era.CE)
return HistoricalDate(current_year, self.era)
def __sub__(self, other):
current_year = self.year
if self.era == Era.CE and other.era == Era.CE:
current_year -= other.year
if current_year <= 0:
current_year -= 1
return HistoricalDate(-current_year, Era.BCE)
return HistoricalDate(current_year, Era.CE)
elif self.era == Era.CE and other.era == Era.BCE:
current_year += other.year
return HistoricalDate(current_year, Era.CE)
elif self.era == Era.BCE and other.era == Era.CE:
current_year += other.year
return HistoricalDate(current_year, Era.BCE)
else:
current_year -= other.year
if current_year <= 0:
current_year -= 1
return HistoricalDate(-current_year, Era.CE)
return HistoricalDate(current_year, Era.BCE)
def __eq__(self, other):
return self.year == other.year
def __ne__(self, other):
return not self == other
def __lt__(self, other):
if self.era == Era.CE and other.era == Era.CE:
return self.year < other.year
elif self.era == Era.BCE and other.era == Era.BCE:
return self.year > other.year
elif self.era == Era.CE and other.era == Era.BCE:
return False
else:
return True
def __gt__(self, other):
if self.era == Era.CE and other.era == Era.CE:
return self.year > other.year
elif self.era == Era.BCE and other.era == Era.BCE:
return self.year < other.year
elif self.era == Era.CE and other.era == Era.BCE:
return True
else:
return False
def __le__(self, other):
return not self > other
def __ge__(self, other):
return not self < other
def century(self):
return ((self.year - 1) // 100) + 1
def millennium(self):
return ((self.year - 1) // 1000) + 1
class HistoricalPeriod:
def __init__(self, begin, end):
self.begin = begin
self.end = end
def __contains__(self, item):
return self.begin <= item < self.end
def duration(self):
return self.end - self.begin
antiquity = HistoricalPeriod(HistoricalDate(3500, Era.BCE), HistoricalDate(476))
middle_ages = HistoricalPeriod(HistoricalDate(476), HistoricalDate(1492))
dark_ages = middle_ages
early_modern_period = HistoricalPeriod(HistoricalDate(1492), HistoricalDate(1789))
nineteenth_age = HistoricalPeriod(HistoricalDate(1789), HistoricalDate(1914))
modernity = HistoricalPeriod(HistoricalDate(1914), HistoricalDate())
historical_periods = [antiquity, middle_ages, early_modern_period, nineteenth_age, modernity]
def read_historical_period(prompt=None, error=None):
if prompt is None:
prompt = 'Your choice: '
if error is None:
error = 'Invalid choice!'
choices = [
Choice('Antiquity', antiquity),
Choice('Middle ages', middle_ages),
Choice('Early modern period', early_modern_period),
Choice('Nineteenth age', nineteenth_age),
Choice('Modernity', modernity)
]
return read_choice(choices, prompt=prompt, error=error, header='Choose a historical period:')
def get_period(event_date):
return [period for period in historical_periods if event_date in period][0]
class HistoricalEvent:
def __init__(self, event_date, description):
self.date = event_date
self.description = description
self.period = get_period(event_date)
def __str__(self):
return f'({self.date}) {self.description}'
class MenuItem:
def __init__(self, name, action):
self.name = name
self.action = action
def invoke(self):
self.action()
class Menu:
def __init__(self, title='', fancy_banner=False):
self.items = []
self.title = title
self.fancy_banner = fancy_banner
def print_banner(self):
if self.fancy_banner:
banner_len = len(self.title)
longest = len(max((item.name for item in self.items), key=len)) if len(self.items) > 0 else 0
if longest < banner_len:
longest = banner_len + 1
digits = len(str(len(self.items)))
special = 2
print('#' * (longest + special + digits))
print(f'#{self.title:^{longest + digits}s}#')
print('#' * (longest + special + digits))
else:
print(self.title)
def add_item(self, name, action):
self.items.append(MenuItem(name, action))
def loop(self):
while True:
self.print_banner()
offset = 0
for offset, item in enumerate(self.items):
print(f'{offset}) {item.name}')
print(f'{offset+1}) Quit')
user_choice = read_int('Your choice: ')
if user_choice == offset+1:
return
if user_choice in range(0, len(self.items)):
self.items[user_choice].invoke()
else:
print('Invalid choice!')
@enum.unique
class QuestionType(enum.Enum):
DATE = enum.auto()
PERIOD = enum.auto()
RANDOM = enum.auto()
class Question:
def __init__(self, name, answer, question_type):
self.name = name
self.answer = answer
self.question_type = question_type
def generate_questions(events, num_questions, question_type):
names = {QuestionType.DATE: 'In which year was happened following event?\n',
QuestionType.PERIOD: 'In which period was happened following event?\n'}
result = []
events = events[0:num_questions]
for event in events:
current_type = question_type
if question_type == QuestionType.RANDOM:
choices = random.choice(list(names.items()))
current_type = choices[0]
name = choices[1]
else:
name = names[question_type]
name += event.description
answers = {QuestionType.DATE: event.date,
QuestionType.PERIOD: event.period}
answer = answers[current_type]
question = Question(name=name, answer=answer, question_type=current_type)
result.append(question)
return result
def ask_question(question):
print(question.name)
answer = None
if question.question_type == QuestionType.DATE:
answer = read_historical_year('Your answer (enter the year in format YYYY [BCE|CE]: ')
elif question.question_type == QuestionType.PERIOD:
answer = read_historical_period('Your answer: ')
if question.answer == answer:
print('Good answer!')
return True
else:
print('Bad answer!')
return False
class EventsCalendar:
def __init__(self):
self.events = []
def add(self, event_date, description):
self.events.append(HistoricalEvent(event_date, description))
def remove_at(self, index):
return self.events.pop(index)
def remove(self, event):
self.events.remove(event)
return event
def search_by_date(self, event_date):
return list(filter(lambda event: event.date == event_date, self.events))
def search_by_period(self, event_period):
return list(filter(lambda event: event.period == event_period, self.events))
def search_by_desc(self, description):
return list(filter(lambda event: description in event.description, self.events))
def all_events(self):
return self.events
def is_present(self, event):
return event in self.events
class EventsCalendarUI:
def __init__(self):
if not os.path.exists(self.data_filename):
self.events_calendar = EventsCalendar()
with open(self.data_filename, 'rb') as app_data:
self.events_calendar = pickle.load(app_data)
def add_event(self):
year = read_historical_year()
description = input('Enter an event description: ')
self.events_calendar.add(year, description)
print('Done!')
def remove_event(self):
events = self.events_calendar.all_events()
if not events:
print('Event list is empty!')
return
year = read_historical_year()
print('Found events:')
events = self.events_calendar.search_by_date(year)
for offset, desc in enumerate(events):
print(f'{offset}) {desc}')
index = read_in_range(prompt='Which one do you want to remove? ',
error='No such element at given index',
lower=0, upper=len(events))
self.events_calendar.remove_at(index)
def list_events(self):
events = self.events_calendar.all_events()
if not events:
print('Events list is empty!')
return
for event in events:
print(event)
def quiz(self):
events = self.events_calendar.all_events()
if not events:
print('Event list is empty!')
return
num_questions = read_in_range('How many questions should I ask? ', lower=1, upper=len(events))
questions = generate_questions(events, num_questions, QuestionType.DATE)
for question in questions:
ask_question(question)
def search_events(self):
events = self.events_calendar.all_events()
if not events:
print('Event list is empty!')
return
def search_events_by_year():
year = read_historical_year()
found_events = self.events_calendar.search_by_date(year)
if not found_events:
print('No such any events at given year.')
return
for event in found_events:
print(event)
def search_events_by_period():
period = read_historical_period()
found_events = self.events_calendar.search_by_period(period)
if not found_events:
print('No such any events at given period.')
return
for event in found_events:
print(event)
def search_events_by_desc():
user_input = input('Enter a fragment of event description: ')
found_events = self.events_calendar.search_by_desc(user_input)
if not found_events:
print('No such any events.')
return
for event in found_events:
print(event)
search_menu = Menu(title='Choose the search criteria')
search_menu.add_item('Search events by year', search_events_by_year)
search_menu.add_item('Search events by period', search_events_by_period)
search_menu.add_item('Search events by description', search_events_by_desc)
search_menu.loop()
def save_events(self):
with open(self.data_filename, 'wb') as app_data:
pickle.dump(self.events_calendar, app_data)
print('Done!')
data_filename = 'events_calendar.pkl'
if __name__ == '__main__':
ui = EventsCalendarUI()
menu = Menu(title='Main menu', fancy_banner=True)
menu.add_item('Quiz', ui.quiz)
menu.add_item('Add a historical event', ui.add_event)
menu.add_item('Delete the historical event', ui.remove_event)
menu.add_item('Search the historical event', ui.search_events)
menu.add_item('List all events', ui.list_events)
menu.add_item('Save events list', ui.save_events)
menu.loop()
print('Good bye!')