import argparse
import configparser
import math
import os
import sys
import ywalk.parser as Parser
from ywalk.types import Mode, Place
from ywalk.graph import Graph
class ParseModeRestriction(argparse.Action):
def __call__(self, parser, namespace, args, option_string=None):
arglist = set(args.split(','))
for arg in arglist:
if arg.upper() not in Mode.__members__:
errx(f'Not a valid Mode: {arg}')
modes = [Mode[arg.upper()] for arg in arglist]
saved = getattr(namespace, self.dest) or []
saved.extend(modes)
setattr(namespace, self.dest, saved)
# pylint: disable=line-too-long
cli = argparse.ArgumentParser()
cli.add_argument('place', type=Place, nargs='*', help='a place in Morrowind')
cli.add_argument('-w', dest='weight_spec', metavar='WEIGHTING_METHOD', help='the weighting method to apply when searching for a route')
cli.add_argument('-P', dest='deny_places', type=Place, action='append', metavar='PLACE', help='avoid traveling through this place')
cli.add_argument('-m', dest='allow_modes', action=ParseModeRestriction, metavar='MODES', help='allow only these modes of travel')
cli.add_argument('-M', dest='deny_modes', action=ParseModeRestriction, metavar='MODES', help='deny these modes of travel')
cli.add_argument('-T', dest='deny_teleport', action='store_true', help='deny teleportation')
def errx(msg):
sys.exit(f'ywalk: {msg}')
def pretty_print_path(path):
origin = path[0].origin
destination = path[-1].destination
time = 0
print(f'Start in {origin}')
for conn in path:
time = time + conn.time
print(f' then {conn.mode.pretty} to {conn.destination}', end='')
if conn.time:
print(f' ({conn.time} {"hour" if conn.time == 1 else "hours"})')
else:
print()
if time:
print(f'Arrive in {destination} after {time} hours.')
def pretty_print_place(place, graph):
def pretty_print_connections(conns):
for conn in sorted(conns, key=lambda c: c.mode.value):
print(conn)
print(f'Direct connections from {place}:')
pretty_print_connections(graph.get_connections_from(place))
print()
print(f'Direct connections to {place}:')
pretty_print_connections(graph.get_connections_to(place))
print()
print ('Reachability map:')
weights, _ = graph.shortest_paths(place)
for destination in sorted(weights.keys(), key=weights.get):
if place == destination:
continue
reachability = "unreachable" if weights[destination] == math.inf else weights[destination]
print(f'{destination}: {reachability}')
def get_xdg_home(xdg_type, fallback):
env = f'XDG_{xdg_type}_HOME'
if env in os.environ:
return os.environ[env]
return os.path.expanduser(fallback)
def apply_predicates(args, graph):
if args.allow_modes:
graph.add_predicate(lambda c: c.mode in args.allow_modes)
if args.deny_modes:
graph.add_predicate(lambda c: c.mode not in args.deny_modes)
if args.deny_teleport:
graph.add_predicate(lambda c: not c.mode.teleport)
if args.deny_places:
graph.add_predicate(lambda c: c.destination not in args.deny_places)
def main():
args = cli.parse_intermixed_args()
config = configparser.ConfigParser()
config.read(os.path.join(get_xdg_home('CONFIG', '~/.config'), 'ywalk', 'config'))
datafile = config.get('misc', 'data', fallback='goty')
datapath = os.path.join(get_xdg_home('DATA', '~/.local/share'), 'ywalk', f'{datafile}.tsv')
graph = Graph()
try:
graph.populate(Parser.parse_tsv(datapath))
except (FileNotFoundError, Parser.InputError, ValueError) as err:
errx(err)
recall = config.get('misc', 'recall', fallback=None)
if recall:
graph.add_recall(Place(recall))
for place in args.place:
if place not in graph:
errx(f'Can\'t find "{place}".')
graph.set_weight(args.weight_spec or config.get('misc', 'weighting_method', fallback=None))
apply_predicates(args, graph)
if not args.place:
for place in sorted(graph.get_places(), key=lambda p: p.name):
print(place)
elif len(args.place) == 1:
pretty_print_place(args.place[0], graph)
elif len(args.place) > 1:
path = graph.find_path(*args.place)
if path is None:
errx(f'Graph exhausted, no connection between {", ".join(map(str, args.place))}')
pretty_print_path(path)
if __name__ == '__main__':
main()