예제 #1
0
    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)
예제 #2
0
    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
예제 #3
0
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
예제 #4
0
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()
예제 #5
0
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
예제 #6
0
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)