Skip to content

Commit

Permalink
Add linting and testing
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyannn committed Jan 14, 2024
1 parent 93acefa commit 3a01b16
Show file tree
Hide file tree
Showing 16 changed files with 242 additions and 60 deletions.
3 changes: 3 additions & 0 deletions .github/linters/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 200
extend-ignore = E203, E704
3 changes: 3 additions & 0 deletions .github/linters/.isort.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[settings]
profile = black
skip_gitignore = true
2 changes: 2 additions & 0 deletions .github/linters/.markdown-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Arbitrary line length
MD013: false
7 changes: 7 additions & 0 deletions .github/linters/.python-lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[MAIN]
jobs = 0
output-format = colorized
exit-zero = True

[MESSAGES CONTROL]
disable = import-error,no-name-in-module
8 changes: 8 additions & 0 deletions .github/linters/.yaml-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extends: default

ignore-from-file: .gitignore

rules:
comments: disable
document-start: disable
line-length: disable
28 changes: 28 additions & 0 deletions .github/workflows/super-linter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This workflow executes several linters on changed files based on languages used in your code base whenever
# you push a code or open a pull request.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/github/super-linter
name: Lint Code Base

on: # yamllint disable-line rule:truthy
push:
branches: ["master"]
pull_request:
branches: ["master"]
jobs:
run-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0

- name: Lint Code Base
uses: github/super-linter@v5
env:
VALIDATE_ALL_CODEBASE: true
DEFAULT_BRANCH: "master"
39 changes: 39 additions & 0 deletions .github/workflows/unittests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Python Unit Tests

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11

- name: Install dependencies in Python 3.11
run: |
python3.11 -m pip install --upgrade pip
python3.11 -m pip install -r requirements.txt
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.12

- name: Install dependencies in Python 3.12
run: |
python3.12 -m pip install --upgrade pip
python3.12 -m pip install -r requirements.txt
- name: Run tests
run: |
python3.11 -m unittest *.py
python3.12 -m unittest *.py
2 changes: 1 addition & 1 deletion .idea/coding.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

160 changes: 111 additions & 49 deletions averages.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# * Feel free to import and use anything from the standard library!
# * You can look at the test cases in the main_test.py file.
# * Feel free to copy the test cases into your IDE of choice
# and copy your code back here. Just make sure it runs.
import re
import unittest
from math import nan, isnan
from statistics import mean
from typing import Optional


# Raise this error for any invalid input, preferably with a clear error message
class ParseError(Exception):
pass

Expand All @@ -21,39 +17,44 @@ def __init__(self, text):
def is_end(self):
return self.pos == len(self.text)

def read(self, expect: Optional[set | str] = None):
if value := self.peek():
self.pos += 1
if expect is not None:
if value not in expect:
raise ValueError("Unexpected value", value)
return value

def peek(self, count=1):
def peek(self, count: int = 1):
return self.text[self.pos : self.pos + count]

def read(self, expect: Optional[str] = None, count=None):
count = count or len(expect or " ")
value = self.peek(count)
if expect is not None and value != expect:
raise ValueError("Unexpected value", value)
self.pos += len(value)
return value

def __str__(self):
return str(self.text[: self.pos]) + f"[{self.peek()}]"
return str(self.text[: self.pos]) + f"<{self.peek()}>"


KEY_VALUE_EXPRESSION = re.compile(r"([A-Za-z0-9]+)=(.*)")


def parse_atom(reader):
if reader.peek(3) == "NaN":
for _ in range(3):
reader.read()
reader.read("NaN", count=3)
return nan

number = None
exponent = None
sign = None

while reader.peek() not in ";]":
match reader.read():
case "+":
if number is not None or exponent is not None:
raise ValueError("Invalid sign position", "+")
if sign is not None:
raise ValueError("Invalid sign combination", sign, "+")
sign = 1
case "-":
if number is not None or exponent is not None:
raise ValueError("Invalid sign position", "+")
if sign is not None:
raise ValueError("Invalid sign combination", sign, "-")
sign = -1
Expand All @@ -68,75 +69,136 @@ def parse_atom(reader):
case anything:
raise ValueError("Unexpected token", anything)

if reader.peek() == ";":
_ = reader.read(";")
if number is None:
raise ValueError("Number should contain at least one digit")

return (sign or +1) * number / 10 ** (exponent or 0)


assert parse_atom(Reader("12")) == 12
assert parse_atom(Reader("5.7")) == 5.7
assert parse_atom(Reader("-4.5555")) == -4.5555
assert isnan(parse_atom(Reader("NaN")))


def parse_nested_list(reader: Reader):
def gen():
_ = reader.read("[")
while True:
match reader.peek():
case "]":
break
case "[":
yield parse_nested_list(reader)
case _:
yield parse_atom(reader)
_ = reader.read("]")
if reader.peek() == "]":
_ = reader.read("]")
else:
while True:
yield (parse_nested_list if reader.peek() == "[" else parse_atom)(
reader
)
match reader.read():
case ";":
continue
case "]":
break
case _:
raise ValueError("Unexpected token when parsing list")

return list(gen())


assert parse_nested_list(Reader("[1.0;2.0;3.0;4.0]")) == [1, 2, 3, 4]
assert parse_nested_list(Reader("[]")) == []


def parse_key_value_pair(pair):
match = KEY_VALUE_EXPRESSION.fullmatch(pair)
if not match:
raise ParseError("Invalid key-value pair", pair)
reader = Reader(match.group(2))
try:
return match.group(1), parse_nested_list(reader)
except ValueError:
value = parse_nested_list(reader)
if reader.peek():
raise ParseError("Extra symbols left in the string")
return match.group(1), value
except ValueError as e:
raise ParseError(
"Error when parsing",
reader,
)
) from e

assert parse_key_value_pair("arg=[0.05]") == ("arg", [0.05])
assert parse_key_value_pair("arg36=[1;NaN]") == ("arg36", [1, nan])

def compute_average_expression(e):
def average_expression(e):
match e:
case []:
return 0
case list(l):
return mean(compute_average_expression(part) for part in l)
return mean(average_expression(part) for part in l)
case value:
return value


def parse_arguments(string):
return [parse_key_value_pair(pair) for pair in string.split()]
string = string.strip()
if not string:
raise ParseError("There should be at least one argument", string)
return [parse_key_value_pair(pair) for pair in string.split() if pair]


def dump_arguments(arguments, accuracy: int) -> str:
return " ".join(f"{arg}={value:0.{accuracy}}" for arg, value in arguments)
return " ".join(f"{arg}={value:.{accuracy}f}" for arg, value in arguments)


# This function shouldn't be renamed so it can be imported in the tests
def parse_compute_averages(input_arguments: str) -> str:
arguments = parse_arguments(input_arguments)
return dump_arguments(
((arg, compute_average_expression(e)) for arg, e in arguments), accuracy=2
((arg, average_expression(e)) for arg, e in arguments), accuracy=2
)


class TestParsing(unittest.TestCase):
def test_parse_atom(self):
self.assertEqual(parse_atom(Reader("12")), 12)
self.assertEqual(parse_atom(Reader("5.7")), 5.7)
self.assertEqual(parse_atom(Reader("-4.5555")), -4.5555)
self.assertEqual(parse_atom(Reader("-0.5678")), -0.5678)
self.assertTrue(isnan(parse_atom(Reader("NaN"))))

def test_malformed_atom(self):
self.assertRaises(ValueError, parse_atom, Reader(".1.2"))
self.assertRaises(ValueError, parse_atom, Reader("--5"))
self.assertRaises(ValueError, parse_atom, Reader("6,7"))
self.assertRaises(ValueError, parse_atom, Reader("nan"))
self.assertRaises(ValueError, parse_atom, Reader("2NaN"))

def test_parse_simple_list(self):
self.assertEqual(parse_nested_list(Reader("[]")), [])
self.assertEqual(parse_nested_list(Reader("[123]")), [123])
self.assertEqual(parse_nested_list(Reader("[1.0;2.0;3.0;4.0]")), [1, 2, 3, 4])

def test_parse_nested_list(self):
self.assertEqual(parse_nested_list(Reader("[[];[]]")), [[], []])
self.assertEqual(
parse_nested_list(Reader("[1.0;[2.0;[3.0]];4.0]")), [1, [2, [3]], 4]
)

def test_malformed_nested_list(self):
def test(expr):
self.assertRaises(ValueError, parse_nested_list, Reader(expr))

test("[;-5]")
test("[[],[]]")
test("[[][]]")
test("[4;]")
test("[NaN5]")

def test_parse_key_value_pair(self):
self.assertEqual(parse_key_value_pair("a0=[0]"), ("a0", [0]))
self.assertEqual(parse_key_value_pair("arg=[0.056]"), ("arg", [0.056]))
self.assertEqual(parse_key_value_pair("arg36=[[1];NaN]"), ("arg36", [[1], nan]))

def test_parse_arguments(self):
self.assertRaises(ParseError, parse_arguments, "")
self.assertEqual(
parse_arguments("arg36=[[1]] a=[5.678]"), [("arg36", [[1]]), ("a", [5.678])]
)

def test_compute_average_expression(self):
self.assertEqual(average_expression([]), 0)
self.assertEqual(average_expression([1, 2, 3]), 2)
self.assertEqual(average_expression([1, [2, [4]], 2]), 2)
self.assertTrue(isnan(average_expression([1, [2, [nan]], 2])))
self.assertEqual(average_expression([1, [2, [4]], [3, [6, [8]]]]), 3)

def test_dump_arguments(self):
self.assertEqual(dump_arguments([("arg", 0.056)], accuracy=2), "arg=0.06")
self.assertEqual(dump_arguments([("a", 1), ("b", .24)], accuracy=1), "a=1.0 b=0.2")


if __name__ == "__main__":
unittest.main()
26 changes: 26 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
_default:
@just --list --unsorted --justfile {{justfile()}} --list-prefix ····

markdown_files := "*.md"
python_files := "*.py"
yaml_files := ".github/*/*.yml"

# format Markdown, YAML and Python files
fmt:
prettier --write -- {{markdown_files}} {{yaml_files}}
isort --settings-path .github/linters/.isort.cfg -- {{python_files}}
black -- {{python_files}}

# lint Markdown, YAML and Python files
lint:
yamllint --config-file .github/linters/.yaml-lint.yml -- {{yaml_files}}
markdownlint --config .github/linters/.markdown-lint.yml -- {{markdown_files}}
prettier --check -- {{markdown_files}} {{yaml_files}}
flake8 --config .github/linters/.flake8 -- {{python_files}}
isort --settings-path .github/linters/.isort.cfg --check --diff -- {{python_files}}
black --diff -- {{python_files}}
pylint --rcfile .github/linters/.python-lint -- {{python_files}}

# test Python files
test:
python -m unittest {{python_files}}
10 changes: 5 additions & 5 deletions lc_0094_binary_in_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
"""

# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right

from typing import List, Optional

Expand Down
2 changes: 2 additions & 0 deletions lc_0212_word_search_original.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List

class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
m, n = len(board), len(board[0])
Expand Down
Loading

0 comments on commit 3a01b16

Please sign in to comment.