def report_errors(self, obj, prelude=None): """Print to stderr all errors applicable to the object. Print to stderr the `strerror` of each error applicable to `obj` preceded by `prelude` (if not given, the hash of `obj`), a colon, and a space. Parameters ---------- obj : hashable The object whose errors are to be reported. prelude : str, optional The text to print before each error report (default the hash of `obj`). """ if prelude is None: prelude = str(hash(obj)) flags = self.log_entry(obj) if not flags: error.error('(no errors)', prelude=prelude) return for err in self.errors: if err.errno & flags: error.error(err.strerror, prelude=prelude)
def autolog(self, obj, report=False): puzzle = obj error_count = 0 # For error reporting name = str(hash(puzzle)) if not puzzle.name else puzzle.name inconsistent_locations = puzzle.inconsistencies() if inconsistent_locations: self.log_error(puzzle, self.INCONSISTENT_BOARD) error_count += 1 if report: # A blank board inconsistent_board = Board() for location in inconsistent_locations: num = puzzle.get_cell(*location) inconsistent_board.set_cell(num, *location) msg = 'puzzle has inconsistencies:\n\n' msg += frmt.strfboard(inconsistent_board, ascii_mode=True) msg += '\n\n' error.error(msg, prelude=name) # See the generator module for references on 17 being the minimum # for a proper Sudoku puzzle if puzzle.clue_count() < 17: self.log_error(puzzle, self.TOO_FEW_CLUES) error_count += 1 if report: msg = 'puzzle contains fewer clues than the 17 required for' msg += ' a proper, single-solution Sudoku board\n' error.error(msg, prelude=name) return error_count
def _retrieve_location(location): # Return given path or, if URL given, path to file downloaded from URL. io_error_msg = None try: # Assume `location` is local path and try to open it for reading with open(location, 'r') as _: pass return location except IOError as err: # pylint: disable=no-member; `strerror` as a `str` has `lower` # If treating `location` as a URL also fails, `io_error_msg` will # be shown; `strerror` allows for nicer formatting than `str(err)` io_error_msg = err.strerror.lower() # A consistently named (to ease testing output) tempfile that includes # the original extension (to ease detecting if an image) ideal_location = _get_temp_filename(location) try: # Assume `location` is a URL download_location, _ = urlretrieve(location, filename=ideal_location) except ValueError as err: if io_error_msg is None: # This should never occur io_error_msg = str(err).lower() error.error(io_error_msg, prelude=location) return None except (URLError, HTTPError) as err: # The `reason` attribute for these is not guaranteed to be a `str` error.error(str(err.reason).lower(), prelude=location) return None return download_location
def main(): """Get puzzle(s) from file, user, or seed and solve each in turn. """ args = _get_parser().parse_args() # Warn if the UTF-8 output will look like garbage on this terminal if not args.ascii and sys.stdout.encoding in ['ascii', 'ANSI_X3.4-1968']: error.error( 'assuming --ascii since your terminal does not seem to' ' support UTF-8 output. To fix permanently, please set' " your locale environment variables and your terminal's" ' character encoding settings appropriately. To force' ' UTF-8 output temporarily, try calling this program' ' using "PYTHONIOENCODING=utf_8 sudb" instead of just' ' "sudb".\n', prelude='warning') args.ascii = True init_commands = [] if not args.no_init: init_path = os.path.expanduser(INIT_FILE) try: with open(init_path, 'r') as init: init_commands = [ line for line in init.read().split('\n') if line ] except IOError: pass command_queue = [] if args.execute is None else args.execute # Flatten the list of puzzle lines lines = None if args.lines is None else [ line for line_list in args.lines for line in line_list ] if args.random is not None and not args.random: # `-r` was used without arguments args.random.append(generator.random_seed()) log = PuzzleErrorLogger() puzzles = importer.get_puzzles(filenames=args.file, lines=lines, seeds=args.random, logger=log) solved_puzzles = 0 if log.error_count() > 0: # There was an import error; print a newline to stderr for spacing error.error('', prelude='') print('{} puzzle{}to solve.'.format(len(puzzles), 's ' if len(puzzles) != 1 else ' ')) for i, puzzle in enumerate(puzzles): _edit_puzzle(puzzle, args.satisfactory, args.minimized, args.symmetrical) skip_solving = False if log.autolog(puzzle, report=True) and log.in_mask( puzzle, log.unsolvable_mask()): # A unsolvable error occured skip_solving = True # A copy of the original is needed to find differences between it # and the final state of the board and for error output original_puzzle = puzzle.duplicate() if skip_solving: pass elif args.auto: auto_solver = Solver(puzzle) if auto_solver.autosolve_without_history(): solved_puzzles += 1 else: options = SolverController.Options() options.ascii = args.ascii solver = SolverController(puzzle, init_commands=init_commands, command_queue=command_queue, options=options) try: if solver.solve(): solved_puzzles += 1 except BaseException: # To make it easier to return to board state at crash print('\nFinal State of Puzzle {} ({}):'.format( i + 1, puzzle.name)) print(puzzle) print() with open(CMDHIST_FILE, 'w+') as cmdhist: cmdhist.write('\n'.join(solver.command_history)) puzzle_line_args = ' '.join(str(original_puzzle).split()) print('Command history saved in "{}". To restore:'.format( CMDHIST_FILE)) print('{} -x "source {}" -l {}'.format(sys.argv[0], CMDHIST_FILE, puzzle_line_args)) print() raise colormap = None if args.difference: colormap = frmt.get_colormap(puzzle.differences(original_puzzle), frmt.Color.GREEN) puzzle_str = frmt.strfboard(puzzle, colormap=colormap, ascii_mode=args.ascii) print('\nEnd Board for Puzzle {}:'.format(i + 1)) print('{}\n({})\n'.format(puzzle_str, puzzle.name)) print('Solved {} of {}.'.format(solved_puzzles, len(puzzles))) if log.error_count() > 0: print() log.print_summary()
def get_puzzles(lines=None, filenames=None, seeds=None, logger=None): """Return puzzles from lines, files, or seeds with optional logging. Return a list of puzzles imported from the lines, filenames, and seeds provided (or from stdin if none provided), and optionally store any errors in the passed error logger. Parameters ---------- lines : list of iterable, optional A list of 9 or more iterables (e.g., str, list, tuple), each of which is imported as a row in a puzzle if it contains exactly 9 elements and no whitespace (default None). filenames : list of str, optional A list of paths to text or image files from which to import puzzles (default None). seeds : list of int, long, or hashable, optional A list of seeds from which to generate puzzles (default None). logger : ErrorLogger instance, optional The logger to use for error accumulating (default None). Returns ------- list of Board instance A list of all puzzles imported from `filenames` and `seeds` (or from stdin if neither given). Notes ----- Aside from the addition of error logging, this is just an integrated version of the other methods available in the module. """ if logger is not None: import_error = error.Error('import error') logger.add_error(import_error) puzzles = [] get_from_stdin = True if lines is not None: get_from_stdin = False puzzles.extend(get_puzzles_from_lines(lines, name='lines argument')) warned_about_image_import = False if filenames is not None: get_from_stdin = False for filename in filenames: if not warned_about_image_import and _is_image(filename): error.error( 'importing from image will likely work only on' ' cleanly cropped images with sharp text and a' ' high-contrast grid, and even then the resulting' ' puzzle may have missing or incorrect clues.\n', prelude='warning') warned_about_image_import = True file_puzzles = get_puzzles_from_file(filename) if logger is not None and not file_puzzles: logger.log_error(filename, import_error) puzzles.extend(file_puzzles) if seeds is not None: get_from_stdin = False puzzles.extend(get_puzzles_from_seeds(seeds)) if get_from_stdin: puzzles = get_puzzles_from_file() return puzzles
def get_puzzles_from_file(filename=None): """Return a list of puzzles drawn from a file, URL, or user input. Return a list of puzzles from the path or URL given in `filename`. If a text file, construct the lines in each puzzle from lines in the file of this form (where any non-numeric, non-whitespace character can be used instead of 0 to represent blanks in the puzzle): 003020600 900305001 001806400 008102900 700000008 006708200 002609500 800203009 005010300 If no filename is given, get the puzzle lines directly from the user. Parameters ---------- filename : str, optional The path or URL to the image (PNG, JPEG, or GIF) file containing a puzzle or the text file containing one or more puzzles (default None). Returns ------- list of Board instance A list of all puzzles found in the given file or entered by the user. """ lines = [] specify_lineno_in_name = True if filename is not None: filename = _retrieve_location(filename) if filename is None: return [] if _is_image(filename): specify_lineno_in_name = False try: from sudb import sudokuimg lines = sudokuimg.puzzle_lines(filename) except ImportError as err: error.error(str(err).lower(), prelude=filename) return [] else: with open(filename, 'r') as puzzle_file: lines = puzzle_file.read().split('\n') else: print( 'Enter the 9 characters in each row of the puzzle(s) from top to', 'bottom. Use 0 or any non-numerical, non-whitespace character to', 'represent a blank in the puzzle. When done, type a period on a', 'line by itself (or just type EOF, e.g., ctrl-D).', sep='\n') print() row = '' while row != '.': try: row = input() lines.append(row) except EOFError: break print() return get_puzzles_from_lines(lines, name=filename, specify_lineno=specify_lineno_in_name)