Skip to content

Commit

Permalink
Implement loading of a Sudoku Board from a file
Browse files Browse the repository at this point in the history
  • Loading branch information
abadger committed May 31, 2022
1 parent 4efe3b8 commit 5c5f675
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ repos:
additional_dependencies:
- mccabe
- pylint-mccabe
- pydantic
args:
- "--load-plugins=pylint.extensions.mccabe"
- "--ignore-patterns=test_.*\\.py"
Expand All @@ -104,6 +105,7 @@ repos:
- id: vulture
args:
- "suds"
- "vulture_false_positives"
- repo: https://github.com/PyCQA/pydocstyle
rev: 6.1.1
hooks:
Expand Down
4 changes: 1 addition & 3 deletions pylintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[MASTER]
ignore=.git
jobs=0
extension-pkg-allow-list=pydantic

[BASIC]
good-names=f,
Expand All @@ -9,6 +10,3 @@ good-names=f,
[LOGGING]
logging-format-style=new
logging-modules=twiggy

[MESSAGES CONTROL]
extension-pkg-whitelist = "pydantic"
11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ description = "A sudoku solver"
home-page = "https://github.com/abadger/suds"
requires-python = "~=3.0"
readme = "README.md"
dependencies = []
dependencies = [
"pydantic",
]

[project.urls]
repository = "https://github.com/abadger/suds"
documentation = "https://readthedocs.io/suds"

[build-system]
# meson dep explicitly added due to https://github.com/mesonbuild/meson/issues/10181
requires = ["meson[ninja]!=0.62.0", "mesonpep517"]
# mesonpep517 from a checkout because 0.2.0 does not understand dependencies
# Use my own fork of mesonpep517 because mesonpep517 uses meson to build so it
# needs to have ninja added to its deps.
requires = ["mesonpep517 @ git+https://gitlab.com/a.badger/mesonpep517@deps-fix", "meson[ninja]!==0.62.0.*"]
build-backend = "mesonpep517.buildapi"

[tool.mesonpep517.metadata]
Expand Down Expand Up @@ -54,4 +59,4 @@ skip_covered = true
show_missing = true

[tool.vulture]
paths = ['suds']
paths = ['suds', 'vulture_false_positives.py']
36 changes: 36 additions & 0 deletions suds/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,42 @@ def __init__(self, old_board=None):
for _count in range(0, self.num_rows):
self._store.append([None] * self.num_columns)

@classmethod
def from_list_of_rows(cls, rows: t.Sequence[t.Sequence[int]]) -> 'SudokuBoard':
"""
Alternate constructor that reads from a list of rows.
The list of rows looks like this:
* A nine element Sequence.
* Each element is a Sequence of nine ints.
* Each int must be from 0 to 9 inclusive.
* 0 represents blank cell.
* Example:
(
(0, 0, 7, 0, 0, 0, 0, 0, 6),
(0, 6, 0, 8, 0, 0, 2, 0, 5),
(0, 0, 8, 0, 6, 9, 3, 0, 0),
(7, 0, 6, 0, 3, 8, 1, 0, 0),
(4, 8, 9, 0, 1, 0, 7, 6, 3),
(0, 0, 3, 9, 7, 0, 5, 0, 8),
(0, 0, 5, 6, 8, 0, 4, 0, 0),
(8, 0, 4, 0, 0, 7, 0, 5, 0),
(6, 0, 0, 0, 0, 0, 9, 0, 0),
)
"""
new_board = cls()

initial_data = {}
for ridx, row in enumerate(rows):
for cidx, cell in enumerate(row):
if cell:
initial_data[(ridx, cidx)] = cell

new_board.update(initial_data)

return new_board

@property
def rows(self):
"""Rows on the Sudoku Board."""
Expand Down
29 changes: 26 additions & 3 deletions suds/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,38 @@
# License: AGPL-3.0-or-later
# See the LICENSE file for more details
"""CLI scripts for suds."""
import argparse
import sys
import typing as t

from .board import SudokuBoard
from .savefile import load_save_file


def parse_args(args: t.Sequence[str]) -> argparse.Namespace:
"""
Parser the commandline.
:arg args: The argument list.
"""
parser = argparse.ArgumentParser()
parser.add_argument('filename', help='The filename containing the json encoded sudoku board')

args: argparse.Namespace = parser.parse_args(args)

return args


def main() -> int:
"""Run suds."""
board = SudokuBoard()
print(board.rows)
board.update({(0, 0): 1, (0, 1): 2})
args = parse_args(sys.argv[1:])
save_data = load_save_file(args.filename)

board: SudokuBoard = SudokuBoard.from_list_of_rows(save_data.board.rows)
print(board.rows)
# board.update({(0, 0): 1, (0, 1): 2})
# print(board.rows)
# board.update({(0, 2): 2})
# print(board.rows)
return 0
2 changes: 1 addition & 1 deletion suds/meson.build
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
src = ['__init__.py', '__main__.py', 'board.py', 'cli.py']
src = ['__init__.py', '__main__.py', 'board.py', 'cli.py', 'savefile.py',]

python.install_sources(
src,
Expand Down
52 changes: 52 additions & 0 deletions suds/savefile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Suds -- A sudoku solver
# Copyright: (C) 2022 Toshio Kuratomi
# License: AGPL-3.0-or-later
# See the LICENSE file for more details
"""Savefile routines for suds."""
import typing as t

import pydantic as p


# pylint: disable=too-few-public-methods
class LocalConfig:
"""Settings for all of our models."""
extra = p.Extra.forbid


class BaseModel(p.BaseModel):
"""BaseModel with our preferred default configuration."""
Config = LocalConfig


class BoardSchema(BaseModel):
"""Structure defining a sudoku board."""
# 9 rows of 9 items containing 0 (meaning the cell is blank) through 9
rows: p.conlist(
p.conlist(p.conint(gt=-1, lt=10), min_items=9, max_items=9), min_items=9, max_items=9)


class SaveFileSchema(BaseModel):
"""
Defines the sructure of a savefile.
.. note:: This is a mapping rather than a list for potential future expansion.
"""
board: BoardSchema


# pylint: enable=too-few-public-methods


def load_save_file(file: t.Union[str, t.TextIO]) -> SaveFileSchema:
"""
Load a SudokBoard from a savefile.
:arg file: This can be a fle name or a file-like object.
"""
if isinstance(file, str):
save_file_data = SaveFileSchema.parse_file(file)
else:
save_file_data = SaveFileSchema.parse_raw(file.read())

return save_file_data
21 changes: 21 additions & 0 deletions tests/test_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,27 @@ def test_create_from_old_board(self):
assert new_board._store[0] is not initial_board._store[0]
assert new_board._store == initial_board._store

def test_create_from_list_of_rows(self):
good_data = [
[0, 0, 7, 0, 0, 0, 0, 0, 6],
[0, 6, 0, 8, 0, 0, 2, 0, 5],
[0, 0, 8, 0, 6, 9, 3, 0, 0],
[7, 0, 6, 0, 3, 8, 1, 0, 0],
[4, 8, 9, 0, 1, 0, 7, 6, 3],
[0, 0, 3, 9, 7, 0, 5, 0, 8],
[0, 0, 5, 6, 8, 0, 4, 0, 0],
[8, 0, 4, 0, 0, 7, 0, 5, 0],
[6, 0, 0, 0, 0, 0, 9, 0, 0],
]

new_board = board.SudokuBoard.from_list_of_rows(good_data)

internal_representation = []
for row in good_data:
internal_representation.append(tuple(c or None for c in row))

assert new_board.rows == _list_to_tuple(internal_representation)


class TestSudokuBoardViews:
"""Test the SudokuBoard properties that return different views of the data."""
Expand Down
23 changes: 22 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import json
import sys

from suds import cli

good_data = [
[0, 0, 7, 0, 0, 0, 0, 0, 6],
[0, 6, 0, 8, 0, 0, 2, 0, 5],
[0, 0, 8, 0, 6, 9, 3, 0, 0],
[7, 0, 6, 0, 3, 8, 1, 0, 0],
[4, 8, 9, 0, 1, 0, 7, 6, 3],
[0, 0, 3, 9, 7, 0, 5, 0, 8],
[0, 0, 5, 6, 8, 0, 4, 0, 0],
[8, 0, 4, 0, 0, 7, 0, 5, 0],
[6, 0, 0, 0, 0, 0, 9, 0, 0],
]


def test_main(monkeypatch, tmpdir):
tmpsavefile = tmpdir / 'save.json'
with open(tmpsavefile, 'w') as f:
f.write(json.dumps({'board': {'rows': good_data}}))

monkeypatch.setattr(sys, 'argv', ['--filename', str(tmpsavefile)])

def test_main():
assert cli.main() == 0
36 changes: 36 additions & 0 deletions tests/test_savefile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import io
import json

from suds import savefile

good_data = {
'board': {
'rows': [
[0, 0, 7, 0, 0, 0, 0, 0, 6],
[0, 6, 0, 8, 0, 0, 2, 0, 5],
[0, 0, 8, 0, 6, 9, 3, 0, 0],
[7, 0, 6, 0, 3, 8, 1, 0, 0],
[4, 8, 9, 0, 1, 0, 7, 6, 3],
[0, 0, 3, 9, 7, 0, 5, 0, 8],
[0, 0, 5, 6, 8, 0, 4, 0, 0],
[8, 0, 4, 0, 0, 7, 0, 5, 0],
[6, 0, 0, 0, 0, 0, 9, 0, 0],
]
}
}

good_data_json = json.dumps(good_data)


def test_load_save_file_filename(tmpdir):
tmpsavefile = tmpdir / 'save.json'
with open(tmpsavefile, 'w', encoding='utf-8') as f:
f.write(good_data_json)

assert savefile.load_save_file(tmpsavefile) == good_data


def test_load_save_file_file_obj():
file_obj = io.StringIO(good_data_json)

assert savefile.load_save_file(file_obj) == good_data
5 changes: 5 additions & 0 deletions vulture_false_positives
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extra # unused variable (/home/badger/programming/playrepo/suds2/suds/savefile.py:14)
Config # unused variable (/home/badger/programming/playrepo/suds2/suds/savefile.py:19)
rows # unused variable (/home/badger/programming/playrepo/suds2/suds/savefile.py:25)
board # unused variable (/home/badger/programming/playrepo/suds2/suds/savefile.py:36)
load_save_file # unused function (/home/badger/programming/playrepo/suds2/suds/savefile.py:42)

0 comments on commit 5c5f675

Please sign in to comment.