Skip to content

Commit ba80463

Browse files
pycalc init
1 parent c1b4999 commit ba80463

File tree

6 files changed

+303
-0
lines changed

6 files changed

+303
-0
lines changed

.travis.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
language: python
2+
python:
3+
- "3.6"
4+
install:
5+
- pip install -r requirements.txt
6+
script:
7+
- cd final_task
8+
- pip install .
9+
- nosetests --cover-branches --with-coverage .
10+
- pycodestyle --max-line-length=120 .
11+
- python ./../pycalc_checker.py
12+
- cd -

final_task/README.md

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Python Programming Language Foundation Hometask
2+
You are proposed to implement pure-python command-line calculator
3+
using **python 3.6**.
4+
5+
6+
## Minimal requirements
7+
Calculator should be a command-line utility which receives mathematical
8+
expression string as an argument and prints evaluated result:
9+
```shell
10+
$ pycalc '2+2*2'
11+
6
12+
```
13+
14+
It should provide the following interface:
15+
```shell
16+
$ pycalc --help
17+
usage: pycalc [-h] EXPRESSION
18+
19+
Pure-python command-line calculator.
20+
21+
positional arguments:
22+
EXPRESSION expression string to evaluate
23+
24+
```
25+
26+
In case of any mistakes in the expression utility should print human-readable
27+
error explanation **with "ERROR: " prefix** and exit with non-zero exit code:
28+
```shell
29+
$ pycalc '15*(25+1'
30+
ERROR: brackets are not balanced
31+
32+
$ pycalc 'func'
33+
ERROR: unknown function 'func'
34+
```
35+
36+
### Mathematical operations calculator must support
37+
* Arithmetic (`+`, `-`, `*`, `/`, `//`, `%`, `^`) (`^` is a power).
38+
* Comparison (`<`, `<=`, `==`, `!=`, `>=`, `>`).
39+
* 2 built-in python functions: `abs` and `round`.
40+
* All functions and constants from standard python module `math` (trigonometry, logarithms, etc.).
41+
42+
### Non-functional requirements
43+
* It is mandatory to use `argparse` module.
44+
* Codebase must be covered with unit tests with at least 70% coverage.
45+
* Usage of external modules is prohibited (python standard library only).
46+
* Usage of **eval** and **exec** is prohibited.
47+
* Usage of **ast** and **tokenize** modules is prohibited.
48+
49+
### Distribution
50+
* Utility should be wrapped into distribution package with `setuptools`.
51+
* This package should export CLI utility named `pycalc`.
52+
53+
### Codestyle
54+
* Docstrings are mandatory for all methods, classes, functions and modules.
55+
* Code must correspond to pep8 (use `pycodestyle` utility for self-check).
56+
* You can set line length up to 120 symbols.
57+
58+
59+
## Optional requirements
60+
These requirement is not mandatory for implementation, but you can get more points for it.
61+
62+
* Support of functions and constants from the modules provided with `-m` or `--use-modules` command-line option.
63+
This option should accept **names** of the modules which are accessible via
64+
[python standard module search paths](https://docs.python.org/3/tutorial/modules.html#the-module-search-path).
65+
Functions and constants from user defined modules have higher priority in case of name conflict then stuff from `math` module or built-in functions.
66+
67+
In this case `pycalc` utility should provide the following interface:
68+
```shell
69+
$ pycalc --help
70+
usage: pycalc [-h] EXPRESSION [-m MODULE [MODULE ...]]
71+
72+
Pure-python command-line calculator.
73+
74+
positional arguments:
75+
EXPRESSION expression string to evaluate
76+
77+
optional arguments:
78+
-h, --help show this help message and exit
79+
-m MODULE [MODULE ...], --use-modules MODULE [MODULE ...]
80+
additional modules to use
81+
```
82+
83+
Usage example:
84+
```python
85+
# my_module.py
86+
def sin(number):
87+
return 42
88+
```
89+
90+
```shell
91+
$ pycalc 'sin(pi/2)'
92+
1.0
93+
94+
$ pycalc 'sin(pi/2)' -m my_module
95+
42
96+
97+
$ pycalc 'time()/3600/24/365' -m time
98+
48.80147332327218
99+
```
100+
101+
---
102+
Implementations will be checked with the latest cPython interpreter of 3.6 branch.
103+
---

final_task/__init__.py

Whitespace-only changes.

final_task/setup.py

Whitespace-only changes.

pycalc_checker.py

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import sys
2+
import subprocess
3+
from math import *
4+
from distutils.util import strtobool
5+
from termcolor import colored
6+
7+
8+
PYCALC_UTIL_NAME = "pycalc"
9+
RETURN_CODE = 0
10+
11+
12+
UNARY_OPERATORS = {
13+
"-13": -13,
14+
"6-(-13)": 6-(-13),
15+
"1---1": 1---1,
16+
"-+---+-1": -+---+-1,
17+
}
18+
19+
OPERATION_PRIORITY = {
20+
"1+2*2": 1+2*2,
21+
"1+(2+3*2)*3": 1+(2+3*2)*3,
22+
"10*(2+1)": 10*(2+1),
23+
"10^(2+1)": 10**(2+1),
24+
"100/3^2": 100/3**2,
25+
"100/3%2^2": 100/3%2**2,
26+
}
27+
28+
29+
FUNCTIONS_AND_CONSTANTS = {
30+
"pi+e": pi+e,
31+
"log(e)": log(e),
32+
"sin(pi/2)": sin(pi/2),
33+
"log10(100)": log10(100),
34+
"sin(pi/2)*111*6": sin(pi/2)*111*6,
35+
"2*sin(pi/2)": 2*sin(pi/2),
36+
"pow(2, 3)": pow(2, 3),
37+
"abs(-5)": abs(-5),
38+
"round(123.4567890)": round(123.4567890),
39+
}
40+
41+
ASSOCIATIVE = {
42+
"102%12%7": 102%12%7,
43+
"100/4/3": 100/4/3,
44+
"2^3^4": 2**3**4,
45+
}
46+
47+
48+
COMPARISON_OPERATORS = {
49+
"1+2*3==1+2*3": eval("1+2*3==1+2*3"),
50+
"e^5>=e^5+1": eval("e**5>=e**5+1"),
51+
"1+2*4/3+1!=1+2*4/3+2": eval("1+2*4/3+1!=1+2*4/3+2"),
52+
}
53+
54+
55+
COMMON_TESTS = {
56+
"(100)": (100),
57+
"666": 666,
58+
"-.1": -.1,
59+
"1/3": 1/3,
60+
"1.0/3.0": 1.0/3.0,
61+
".1 * 2.0^56.0": .1 * 2.0**56.0,
62+
"e^34": e**34,
63+
"(2.0^(pi/pi+e/e+2.0^0.0))": (2.0**(pi/pi+e/e+2.0**0.0)),
64+
"(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)": (2.0**(pi/pi+e/e+2.0**0.0))**(1.0/3.0),
65+
"sin(pi/2^1) + log(1*4+2^2+1, 3^2)": sin(pi/2**1) + log(1*4+2**2+1, 3**2),
66+
"10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5": 10*e**0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5,
67+
"sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)": sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+cos(sin(sin(34.0-2.0**2.0))))--cos(1.0)--cos(0.0)**3.0),
68+
"2.0^(2.0^2.0*2.0^2.0)": 2.0**(2.0**2.0*2.0**2.0),
69+
"sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))": sin(e**log(e**e**sin(23.0),45.0) + cos(3.0+log10(e**-e))),
70+
}
71+
72+
73+
ERROR_CASES = [
74+
"",
75+
"+",
76+
"1-",
77+
"1 2",
78+
"==7",
79+
"1 + 2(3 * 4))",
80+
"((1+2)",
81+
"1 + 1 2 3 4 5 6 ",
82+
"log100(100)",
83+
"------",
84+
"5 > = 6",
85+
"5 / / 6",
86+
"6 < = 6",
87+
"6 * * 6",
88+
"(((((",
89+
"abs",
90+
"pow(2, 3, 4)",
91+
]
92+
93+
94+
PASS_TEMPLATE = "Test"
95+
FAIL_TEMPLATE = ""
96+
97+
98+
def trunc_string(string):
99+
return (string[:40] + '..') if len(string) > 40 else string
100+
101+
102+
def call_command(command, positional_params, optional_params=""):
103+
params = [command, "--", positional_params] if not optional_params else [command, optional_params, " -- ", positional_params]
104+
result = subprocess.run(params, stdout=subprocess.PIPE)
105+
return result.stdout.decode('utf-8')
106+
107+
108+
def check_results(keys: dict, required=True, user_module=""):
109+
global RETURN_CODE
110+
for command, expected_result in keys.items():
111+
result = call_command(PYCALC_UTIL_NAME, command, optional_params=user_module)
112+
try:
113+
converted_result = float(result)
114+
except Exception:
115+
try:
116+
converted_result = bool(strtobool(result[:-1]))
117+
except Exception:
118+
print("{: <45} | Result: {}".format(
119+
command,
120+
"{}: Invalid output: {} | Expected: {}".format(colored("FAIL", "red"), result, expected_result))
121+
)
122+
if required:
123+
RETURN_CODE = 1
124+
continue
125+
126+
if round(expected_result, 2) == round(converted_result, 2):
127+
print("{: <45} | Result: {}".format(trunc_string(command), colored("PASS", "green")))
128+
else:
129+
print("{: <45} | Result: {}".format(
130+
command,
131+
"{}: Invalid output: {} | Expected: {}".format(colored("FAIL", "red"), converted_result, expected_result))
132+
)
133+
if required:
134+
RETURN_CODE = 1
135+
136+
137+
def check_error_results(keys: list, required=True):
138+
global RETURN_CODE
139+
for command in keys:
140+
result = call_command(PYCALC_UTIL_NAME, command)
141+
if result.startswith("ERROR:"):
142+
print("{: <45} | Result: {}".format(trunc_string(command), colored("PASS", "green")))
143+
else:
144+
print("{: <45} | Result: {}".format(trunc_string(command),
145+
"{}, expected correct error handling".format(colored("FAIL", "red"))))
146+
if required:
147+
RETURN_CODE = 1
148+
149+
150+
def main():
151+
print(colored("************** Checking minimal requirements **************\n", "green"))
152+
print("Checking unary operators...")
153+
check_results(UNARY_OPERATORS)
154+
print()
155+
156+
print("Checking operation priority...")
157+
check_results(OPERATION_PRIORITY)
158+
print()
159+
160+
print("Checking functions and constants...")
161+
check_results(FUNCTIONS_AND_CONSTANTS)
162+
print()
163+
164+
print("Checking associative...")
165+
check_results(ASSOCIATIVE)
166+
print()
167+
168+
print("Checking comparison operators...")
169+
check_results(COMPARISON_OPERATORS)
170+
print()
171+
172+
print("Checking common tests...")
173+
check_results(COMMON_TESTS)
174+
print()
175+
176+
print("Checking error cases...")
177+
check_error_results(ERROR_CASES)
178+
print()
179+
180+
sys.exit(RETURN_CODE)
181+
182+
183+
if __name__ == '__main__':
184+
main()

requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pycodestyle==2.4.0
2+
nose==1.3.7
3+
coverage==4.5.1
4+
termcolor==1.1.0

0 commit comments

Comments
 (0)