Inital build with AutoCI/CD

This commit is contained in:
Zevaryx 2021-05-05 17:37:20 -06:00
commit 8a335a7020
13 changed files with 755 additions and 0 deletions

138
.gitignore vendored Normal file
View file

@ -0,0 +1,138 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

11
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,11 @@
image: python:rc
cache:
paths:
- ~/.cache/pip/
before_script:
- python -V
tests:
script:
- python -m unittest discover tests

0
calculator/__init__.py Normal file
View file

51
calculator/interpreter.py Normal file
View file

@ -0,0 +1,51 @@
from .nodes import *
from .values import Number
class Interpreter:
def visit(self, node):
method_name = f"visit_{type(node).__name__}"
method = getattr(self, method_name)
return method(node)
def visit_NumberNode(self, node):
return Number(node.value)
def visit_AddNode(self, node):
return Number(
self.visit(node.node_a).value + self.visit(node.node_b).value
)
def visit_SubtractNode(self, node):
return Number(
self.visit(node.node_a).value - self.visit(node.node_b).value
)
def visit_MultiplyNode(self, node):
return Number(
self.visit(node.node_a).value * self.visit(node.node_b).value
)
def visit_DivideNode(self, node):
try:
return Number(
self.visit(node.node_a).value / self.visit(node.node_b).value
)
except:
raise Exception("Runtime Math Error")
def visit_PowerNode(self, node):
return Number(
self.visit(node.node_a).value ** self.visit(node.node_b).value
)
def visit_ModuloNode(self, node):
return Number(
self.visit(node.node_a).value % self.visit(node.node_b).value
)
def visit_PlusNode(self, node):
return self.visit(node.node)
def visit_MinusNode(self, node):
return Number(-self.visit(node.node).value)

77
calculator/lexer.py Normal file
View file

@ -0,0 +1,77 @@
from .tokens import Token, TokenType
from string import digits
WHITESPACE = " \r\n\t"
class Lexer:
def __init__(self, text):
self.text = iter(text)
self.advance()
def advance(self):
try:
self.current_char = next(self.text)
except StopIteration:
self.current_char = None
def generate_tokens(self):
while self.current_char is not None:
match self.current_char:
case x if x in WHITESPACE:
self.advance()
case x if x in digits or x == ".":
yield self.generate_number()
case "+":
self.advance()
yield Token(TokenType.PLUS)
case "-":
self.advance()
yield Token(TokenType.MINUS)
case "*":
self.advance()
yield Token(TokenType.MULTIPLY)
case "/":
self.advance()
yield Token(TokenType.DIVIDE)
case "(":
self.advance()
yield Token(TokenType.LPAREN)
case ")":
self.advance()
yield Token(TokenType.RPAREN)
case "^":
self.advance()
yield Token(TokenType.POWER)
case "%":
self.advance()
yield Token(TokenType.MODULO)
case _:
raise Exception(f"Illegal Character: '{self.current_char}'")
def generate_number(self):
decimal_pt_count = 0
number_str = self.current_char
self.advance()
while self.current_char is not None and (
self.current_char == "." or self.current_char in digits
):
if self.current_char == ".":
decimal_pt_count += 1
if decimal_pt_count > 1:
break
number_str += self.current_char
self.advance()
if number_str.startswith("."):
number_str = "0" + number_str
if number_str.endswith("."):
number_str += "0"
return Token(
TokenType.NUMBER,
float(number_str) if "." in number_str else int(number_str),
)

79
calculator/nodes.py Normal file
View file

@ -0,0 +1,79 @@
from dataclasses import dataclass
@dataclass
class NumberNode:
value: float | int
def __repr__(self):
return str(self.value)
@dataclass
class AddNode:
node_a: any
node_b: any
def __repr__(self):
return f"({self.node_a} + {self.node_b})"
@dataclass
class SubtractNode:
node_a: any
node_b: any
def __repr__(self):
return f"({self.node_a} - {self.node_b})"
@dataclass
class MultiplyNode:
node_a: any
node_b: any
def __repr__(self):
return f"({self.node_a} * {self.node_b})"
@dataclass
class DivideNode:
node_a: any
node_b: any
def __repr__(self):
return f"({self.node_a} / {self.node_b})"
@dataclass
class PowerNode:
node_a: any
node_b: any
def __repr__(self):
return f"({self.node_a} ^ {self.node_b})"
@dataclass
class ModuloNode:
node_a: any
node_b: any
def __repr__(self):
return f"({self.node_a} % {self.node_b})"
@dataclass
class PlusNode:
node: any
def __repr__(self):
return f"(+{self.node})"
@dataclass
class MinusNode:
node: any
def __repr__(self):
return f"(-{self.node})"

94
calculator/parser_.py Normal file
View file

@ -0,0 +1,94 @@
from .tokens import TokenType
from .nodes import *
class Parser:
def __init__(self, tokens):
self.tokens = iter(tokens)
self.advance()
def raise_error(self, e = None):
if e:
raise Exception(f"Error: {e}")
raise Exception("Invalid syntax")
def advance(self):
try:
self.current_token = next(self.tokens)
except StopIteration:
self.current_token = None
def parse(self):
if self.current_token is None:
return
result = self.expr()
if self.current_token is not None:
self.raise_error("Current token is not None")
return result
def expr(self):
result = self.term()
while self.current_token is not None and self.current_token.type in (
TokenType.PLUS,
TokenType.MINUS,
):
match self.current_token.type:
case TokenType.PLUS:
self.advance()
result = AddNode(result, self.term())
case TokenType.MINUS:
self.advance()
result = SubtractNode(result, self.term())
return result
def term(self):
result = self.factor()
while self.current_token is not None and self.current_token.type in (
TokenType.MULTIPLY,
TokenType.DIVIDE,
TokenType.POWER,
TokenType.MODULO,
):
match self.current_token.type:
case TokenType.MULTIPLY:
self.advance()
result = MultiplyNode(result, self.factor())
case TokenType.DIVIDE:
self.advance()
result = DivideNode(result, self.factor())
case TokenType.POWER:
self.advance()
result = PowerNode(result, self.factor())
case TokenType.MODULO:
self.advance()
result = ModuloNode(result, self.factor())
return result
def factor(self):
token = self.current_token
match token.type:
case TokenType.LPAREN:
self.advance()
result = self.expr()
if self.current_token.type != TokenType.RPAREN:
self.raise_error()
self.advance()
return result
case TokenType.NUMBER:
self.advance()
return NumberNode(token.value)
case TokenType.PLUS:
self.advance()
return PlusNode(self.factor())
case TokenType.MINUS:
self.advance()
return MinusNode(self.factor())
self.raise_error("Invalid factor token")

23
calculator/tokens.py Normal file
View file

@ -0,0 +1,23 @@
from enum import Enum
from dataclasses import dataclass
class TokenType(Enum):
NUMBER = 0
PLUS = 1
MINUS = 2
MULTIPLY = 3
DIVIDE = 4
LPAREN = 5
RPAREN = 6
POWER = 7
MODULO = 8
@dataclass
class Token:
type: TokenType
value: any = None
def __repr__(self):
return self.type.name + (f":{self.value}" if self.value else "")

9
calculator/values.py Normal file
View file

@ -0,0 +1,9 @@
from dataclasses import dataclass
@dataclass
class Number:
value: int | float
def __repr__(self):
return str(self.value)

25
main.py Normal file
View file

@ -0,0 +1,25 @@
import sys
from calculator.interpreter import Interpreter
from calculator.lexer import Lexer
from calculator.parser_ import Parser
if __name__ == "__main__":
MIN_PYTHON = (3, 10)
if sys.version_info < MIN_PYTHON:
sys.exit("Python {}.{} or later is required".format(*MIN_PYTHON))
while True:
try:
text = input(">> ")
lexer = Lexer(text)
tokens = lexer.generate_tokens()
parser = Parser(tokens)
tree = parser.parse()
if not tree:
continue
interpreter = Interpreter()
value = interpreter.visit(tree)
print(value)
except Exception as e:
print(e)

59
tests/test_interpreter.py Normal file
View file

@ -0,0 +1,59 @@
import unittest
from calculator.nodes import (
NumberNode,
AddNode,
SubtractNode,
MultiplyNode,
DivideNode,
PowerNode,
ModuloNode,
)
from calculator.interpreter import Interpreter
from calculator.values import Number
class TestInterpreter(unittest.TestCase):
def test_numbers(self):
val = Interpreter().visit(NumberNode(42))
self.assertEqual(val, Number(42))
def test_individual_operations(self):
val = Interpreter().visit(AddNode(NumberNode(21), NumberNode(21)))
self.assertEqual(val, Number(42))
val = Interpreter().visit(SubtractNode(NumberNode(21), NumberNode(21)))
self.assertEqual(val, Number(0))
val = Interpreter().visit(MultiplyNode(NumberNode(21), NumberNode(21)))
self.assertEqual(val, Number(441))
val = Interpreter().visit(DivideNode(NumberNode(21), NumberNode(21)))
self.assertEqual(val, Number(1))
val = Interpreter().visit(PowerNode(NumberNode(21), NumberNode(21)))
self.assertEqual(val, Number(5842587018385982521381124421))
val = Interpreter().visit(ModuloNode(NumberNode(21), NumberNode(21)))
self.assertEqual(val, Number(0))
with self.assertRaises(Exception):
Interpreter().visit(DivideNode(NumberNode(42), NumberNode(0)))
def test_full_expression(self):
tree = ModuloNode(
SubtractNode(
AddNode(
PowerNode(NumberNode(10), NumberNode(2)),
AddNode(
DivideNode(NumberNode(21), NumberNode(7)),
NumberNode(7),
),
),
MultiplyNode(NumberNode(5), NumberNode(2)),
),
NumberNode(100),
)
result = Interpreter().visit(tree)
self.assertEqual(result, Number(0.0))

75
tests/test_lexer.py Normal file
View file

@ -0,0 +1,75 @@
import unittest
from calculator.lexer import Lexer
from calculator.tokens import Token, TokenType
class TestLexer(unittest.TestCase):
def test_empty(self):
tokens = list(Lexer("").generate_tokens())
self.assertEqual(tokens, [])
def test_whitespace(self):
tokens = list(Lexer("\t\n \t\t\n\n\r\r").generate_tokens())
self.assertEqual(tokens, [])
def test_numbers(self):
tokens = list(Lexer("123 123.456 123. .456 .").generate_tokens())
self.assertEqual(
tokens,
[
Token(TokenType.NUMBER, 123),
Token(TokenType.NUMBER, 123.456),
Token(TokenType.NUMBER, 123.0),
Token(TokenType.NUMBER, 0.456),
Token(TokenType.NUMBER, 0.0),
],
)
def test_operators(self):
tokens = list(Lexer("+-*/^%").generate_tokens())
self.assertEqual(
tokens,
[
Token(TokenType.PLUS),
Token(TokenType.MINUS),
Token(TokenType.MULTIPLY),
Token(TokenType.DIVIDE),
Token(TokenType.POWER),
Token(TokenType.MODULO),
],
)
def test_parens(self):
tokens = list(Lexer("()").generate_tokens())
self.assertEqual(
tokens, [Token(TokenType.LPAREN), Token(TokenType.RPAREN)]
)
def test_all(self):
tokens = list(
Lexer("(10 ^ 2 + (21 / 7 + 7) - 5 * 2) % 100").generate_tokens()
)
self.assertEqual(
tokens,
[
Token(TokenType.LPAREN),
Token(TokenType.NUMBER, 10),
Token(TokenType.POWER),
Token(TokenType.NUMBER, 2),
Token(TokenType.PLUS),
Token(TokenType.LPAREN),
Token(TokenType.NUMBER, 21),
Token(TokenType.DIVIDE),
Token(TokenType.NUMBER, 7),
Token(TokenType.PLUS),
Token(TokenType.NUMBER, 7),
Token(TokenType.RPAREN),
Token(TokenType.MINUS),
Token(TokenType.NUMBER, 5),
Token(TokenType.MULTIPLY),
Token(TokenType.NUMBER, 2),
Token(TokenType.RPAREN),
Token(TokenType.MODULO),
Token(TokenType.NUMBER, 100),
],
)

114
tests/test_parser.py Normal file
View file

@ -0,0 +1,114 @@
import unittest
from calculator.tokens import Token, TokenType
from calculator.parser_ import Parser
from calculator.nodes import (
NumberNode,
AddNode,
SubtractNode,
MultiplyNode,
DivideNode,
PowerNode,
ModuloNode,
)
class TestParser(unittest.TestCase):
def test_empty(self):
tokens = []
node = Parser(tokens).parse()
self.assertEqual(node, None)
def test_numbers(self):
tokens = [Token(TokenType.NUMBER, 100)]
node = Parser(tokens).parse()
self.assertEqual(node, NumberNode(100))
def test_individual_operations(self):
tokens = [
Token(TokenType.NUMBER, 21),
Token(TokenType.PLUS),
Token(TokenType.NUMBER, 23),
]
node = Parser(tokens).parse()
self.assertEqual(node, AddNode(NumberNode(21), NumberNode(23)))
tokens = [
Token(TokenType.NUMBER, 21),
Token(TokenType.MINUS),
Token(TokenType.NUMBER, 23),
]
node = Parser(tokens).parse()
self.assertEqual(node, SubtractNode(NumberNode(21), NumberNode(23)))
tokens = [
Token(TokenType.NUMBER, 21),
Token(TokenType.MULTIPLY),
Token(TokenType.NUMBER, 23),
]
node = Parser(tokens).parse()
self.assertEqual(node, MultiplyNode(NumberNode(21), NumberNode(23)))
tokens = [
Token(TokenType.NUMBER, 21),
Token(TokenType.DIVIDE),
Token(TokenType.NUMBER, 23),
]
node = Parser(tokens).parse()
self.assertEqual(node, DivideNode(NumberNode(21), NumberNode(23)))
tokens = [
Token(TokenType.NUMBER, 21),
Token(TokenType.POWER),
Token(TokenType.NUMBER, 23),
]
node = Parser(tokens).parse()
self.assertEqual(node, PowerNode(NumberNode(21), NumberNode(23)))
tokens = [
Token(TokenType.NUMBER, 21),
Token(TokenType.MODULO),
Token(TokenType.NUMBER, 23),
]
node = Parser(tokens).parse()
self.assertEqual(node, ModuloNode(NumberNode(21), NumberNode(23)))
def test_full_expression(self):
tokens = [
Token(TokenType.LPAREN),
Token(TokenType.NUMBER, 10),
Token(TokenType.POWER),
Token(TokenType.NUMBER, 2),
Token(TokenType.PLUS),
Token(TokenType.LPAREN),
Token(TokenType.NUMBER, 21),
Token(TokenType.DIVIDE),
Token(TokenType.NUMBER, 7),
Token(TokenType.PLUS),
Token(TokenType.NUMBER, 7),
Token(TokenType.RPAREN),
Token(TokenType.MINUS),
Token(TokenType.NUMBER, 5),
Token(TokenType.MULTIPLY),
Token(TokenType.NUMBER, 2),
Token(TokenType.RPAREN),
Token(TokenType.MODULO),
Token(TokenType.NUMBER, 100),
]
node = Parser(tokens).parse()
self.assertEqual(
node,
ModuloNode(
SubtractNode(
AddNode(
PowerNode(NumberNode(10), NumberNode(2)),
AddNode(
DivideNode(NumberNode(21), NumberNode(7)),
NumberNode(7),
),
),
MultiplyNode(NumberNode(5), NumberNode(2)),
),
NumberNode(100),
),
)