Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing #4

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
71bf94c
Add packaging provisions
MarkKoz Jun 11, 2019
200f91e
Fix circular import
MarkKoz Jul 22, 2019
3ebe55d
Make SerializableSequence a generic type
MarkKoz Jul 22, 2019
7dad08d
Add mypy and visualization.json to gitignore
MarkKoz Jul 23, 2019
c799cab
Fix super()-related TypeError in Array2D.create()
MarkKoz Jul 23, 2019
83590a7
Fix using same length variable for both dimensions of Array2D
MarkKoz Jul 23, 2019
31e9b53
Add randomizer tests
MarkKoz Jul 24, 2019
60f8b5d
Fix Commander object count tracking
MarkKoz Jul 25, 2019
2c3316f
Add Commander tests
MarkKoz Jul 26, 2019
b505a7d
Minor refactor to Commander tests
MarkKoz Jul 26, 2019
c2728e5
Add Layout tests
MarkKoz Jul 26, 2019
dcf9599
Test for undefined arguments for Commander.command()
MarkKoz Jul 26, 2019
7a43d3d
Add Tracer tests
MarkKoz Jul 28, 2019
1c37e4b
Refactor Commander tests
MarkKoz Jul 28, 2019
818fea1
Add LogTracer tests
MarkKoz Jul 28, 2019
4708103
Add Array2DTracer tests
MarkKoz Jul 28, 2019
845f70b
Add Array1DTracer tests
MarkKoz Jul 28, 2019
6ea7a2d
Return self from GraphTracer.directed()
MarkKoz Jul 28, 2019
ae25027
Add GraphTracer tests
MarkKoz Jul 28, 2019
060fca0
Add helper function for tests
MarkKoz Aug 2, 2019
0a69697
Add missing Graph edge tests
MarkKoz Aug 2, 2019
190e549
Configure coverage.py
MarkKoz Aug 2, 2019
75e641b
Refactor execute() to make code more testable
MarkKoz Aug 27, 2019
9a9168d
Add a test for create_json_file
MarkKoz Aug 27, 2019
d7fdacb
Replace requests with urllib
MarkKoz Aug 27, 2019
91723d7
Add get_url tests
MarkKoz Aug 27, 2019
cb30d80
Mock urlopen for get_url test
MarkKoz Aug 27, 2019
34724a0
Exclude atexit from coverage
MarkKoz Aug 27, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[run]
branch = True
source = algorithm_visualizer

[report]
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
@atexit
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,9 @@ ENV/
env.bak/
venv.bak/

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

visualization.json
54 changes: 39 additions & 15 deletions algorithm_visualizer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import atexit
import json
import os
import webbrowser
from pathlib import Path
from urllib.request import HTTPError, Request, urlopen

from . import randomize as Randomize
from .commander import Commander
from .layouts import *
from .tracers import *
from .types import PathLike

__all__ = (
"Randomize", "Commander",
Expand All @@ -14,23 +18,43 @@
)


def create_json_file(path: PathLike = "./visualization.json"):
commands = json.dumps(Commander.commands, separators=(",", ":"))
with Path(path).open("w", encoding="UTF-8") as file:
file.write(commands)


def get_url() -> str:
url = "https://algorithm-visualizer.org/api/visualizations"
commands = json.dumps(Commander.commands, separators=(",", ":")).encode('utf-8')
request = Request(
url,
method="POST",
data=commands,
headers={
"Content-type": "application/json; charset=utf-8",
"Content-Length": len(commands)
}
)
response = urlopen(request)

if response.status == 200:
return response.read().decode('utf-8')
else:
raise HTTPError(
url=url,
code=response.status,
msg="Failed to retrieve the scratch URL: non-200 response",
hdrs=dict(response.info()),
fp=None
)


@atexit.register
def execute():
commands = json.dumps(Commander.commands, separators=(",", ":"))
if os.getenv("ALGORITHM_VISUALIZER"):
with open("visualization.json", "w", encoding="UTF-8") as file:
file.write(commands)
create_json_file()
else:
import requests
import webbrowser

response = requests.post(
"https://algorithm-visualizer.org/api/visualizations",
headers={"Content-type": "application/json"},
data=commands
)
url = get_url()
webbrowser.open(url)

if response.status_code == 200:
webbrowser.open(response.text)
else:
raise requests.HTTPError(response=response)
4 changes: 2 additions & 2 deletions algorithm_visualizer/commander.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Commander:
commands: List[Dict[str, Serializable]] = []

def __init__(self, *args: Serializable):
self._objectCount += 1
Commander._objectCount += 1
self.key = self._keyRandomizer.create()
self.command(self.__class__.__name__, *args)

Expand All @@ -38,5 +38,5 @@ def command(self, method: str, *args):
self._command(self.key, method, *args)

def destroy(self):
self._objectCount -= 1
Commander._objectCount -= 1
self.command("destroy")
13 changes: 9 additions & 4 deletions algorithm_visualizer/randomize.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
class _Randomizer(metaclass=abc.ABCMeta):
@abc.abstractmethod
def create(self) -> NoReturn:
raise NotImplementedError
raise NotImplementedError # pragma: no cover


class Integer(_Randomizer):
Expand Down Expand Up @@ -60,15 +60,16 @@ def create(self) -> List:

class Array2D(Array1D):
def __init__(self, N: int = 10, M: int = 10, randomizer: _Randomizer = Integer()):
super().__init__(N, randomizer)
self._M = M
super().__init__(M, randomizer)
self._M = N

def sorted(self, sorted: bool = True) -> "Array2D":
self._sorted = sorted
return self

def create(self) -> List[List]:
return [super().create() for _ in range(self._N)]
# Explicitly pass args to super() to avoid a TypeError (BPO 26495).
return [super(Array2D, self).create() for _ in range(self._M)]


class Graph(_Randomizer):
Expand All @@ -92,15 +93,19 @@ def create(self) -> List[List]:
for i in range(self._N):
for j in range(self._N):
if i == j:
# Vertex can't have an edge to itself (no loops)
graph[i][j] = 0
elif self._directed or i < j:
if random.random() >= self._ratio:
# Don't create an edge if the ratio is exceeded
graph[i][j] = 0
elif self._weighted:
graph[i][j] = self._randomizer.create()
else:
graph[i][j] = 1
else:
# Edge is the same for both its vertices if it is not directed
# In such case the initial weight for the edge is set above when i < j
graph[i][j] = graph[j][i]

return graph
8 changes: 6 additions & 2 deletions algorithm_visualizer/tracers/array1d.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from . import chart
from typing import TYPE_CHECKING

from .array2d import Array2DTracer
from algorithm_visualizer.types import Serializable, SerializableSequence, UNDEFINED

if TYPE_CHECKING:
from .chart import ChartTracer


class Array1DTracer(Array2DTracer):
def set(self, array1d: SerializableSequence[Serializable] = UNDEFINED):
Expand All @@ -19,5 +23,5 @@ def select(self, sx: int, ex: int = UNDEFINED):
def deselect(self, sx: int, ex: int = UNDEFINED):
self.command("deselect", sx, ex)

def chart(self, chartTracer: "chart.ChartTracer"):
def chart(self, chartTracer: "ChartTracer"):
self.command("chart", chartTracer.key)
1 change: 1 addition & 0 deletions algorithm_visualizer/tracers/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def set(self, array2d: SerializableSequence[SerializableSequence[Serializable]]

def directed(self, isDirected: bool = UNDEFINED):
self.command("directed", isDirected)
return self

def weighted(self, isWeighted: bool = UNDEFINED) -> "GraphTracer":
self.command("weighted", isWeighted)
Expand Down
17 changes: 13 additions & 4 deletions algorithm_visualizer/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from typing import Union
from pathlib import PurePath
from typing import Any, Dict, List, Tuple, TypeVar, Union

# Types which are serializable by the default JSONEncoder
Serializable = Union[dict, list, tuple, str, int, float, bool, None]
SerializableSequence = Union[list, tuple]

PathLike = Union[str, bytes, PurePath]
Number = Union[int, float]

# Types which are serializable by the default JSONEncoder
# Recursive types aren't supported yet. See https://github.com/python/mypy/issues/731
_Keys = Union[str, int, float, bool, None]
_Collections = Union[Dict[_Keys, Any], List[Any], Tuple[Any]]
Serializable = Union[_Collections, _Keys]

_T = TypeVar("_T", _Collections, _Keys)
SerializableSequence = Union[List[_T], Tuple[_T]]


class Undefined:
pass
Expand Down
9 changes: 9 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pip >= 19.1.1

# Packaging and Publishing
# Minimum versions for Markdown support
setuptools >= 38.6.0
twine >= 1.12.0 # For twine check
wheel >= 0.31.0

coverage ~= 4.5
50 changes: 50 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import setuptools
from pathlib import Path

readme_path = Path(__file__).with_name("README.md")
with open(readme_path, encoding="utf-8") as f:
long_description = f.read()

setuptools.setup(
name="algorithm-visualizer",
version="0.1.0",
license="MIT",

author="Example Author",
author_email="[email protected]",

description="A visualization library for Python.",
long_description=long_description,
long_description_content_type="text/markdown",

keywords="algorithm data-structure visualization animation",
classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Environment :: Web Environment",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7"
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Programming Language :: Python :: Implementation :: Stackless",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Scientific/Engineering :: Visualization"
],

url="https://algorithm-visualizer.org",
project_urls={
"Documentation": "https://github.com/algorithm-visualizer/algorithm-visualizer/wiki",
"Issue Tracker": "https://github.com/algorithm-visualizer/tracers.py/issues",
"Source": "https://github.com/algorithm-visualizer/tracers.py"
},

packages=setuptools.find_packages(),
python_requires=">=3.5"
)
22 changes: 22 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import unittest
from typing import Optional, Union

from algorithm_visualizer import Commander
from algorithm_visualizer.types import Serializable, Undefined


class CommanderTestCase(unittest.TestCase):
def assertCommandEqual(
self,
method: str,
*args: Union[Serializable, Undefined],
key: Optional[str] = None
):
cmd = Commander.commands[-1]
expected_cmd = {
"key": key,
"method": method,
"args": list(args),
}

self.assertEqual(expected_cmd, cmd)
50 changes: 50 additions & 0 deletions tests/test_commander.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from algorithm_visualizer import commander
from algorithm_visualizer import Commander
from algorithm_visualizer.types import UNDEFINED

from tests import CommanderTestCase


class CommanderTests(CommanderTestCase):
def setUp(self):
self.commander = Commander()

def test_commander_create(self):
old_count = Commander._objectCount
args = [1, 2, 3]
cmder = Commander(*args)

self.assertEqual(old_count + 1, Commander._objectCount)
self.assertCommandEqual("Commander", *args, key=cmder.key)

def test_commander_max_commands(self):
old_cmds = Commander.commands
Commander.commands = [None for _ in range(commander._MAX_COMMANDS)]

with self.assertRaisesRegex(RuntimeError, "Too Many Commands"):
self.commander.command("foo")

Commander.commands = old_cmds

def test_commander_max_objects(self):
old_count = Commander._objectCount
Commander._objectCount = 200

with self.assertRaisesRegex(RuntimeError, "Too Many Objects"):
Commander()

Commander._objectCount = old_count

def test_commander_command(self):
method = "foo"
args = [["bar", "baz"], 12]
self.commander.command(method, *args, UNDEFINED)

self.assertCommandEqual(method, *args, key=self.commander.key)

def test_commander_destroy(self):
old_count = Commander._objectCount
self.commander.destroy()

self.assertEqual(old_count - 1, Commander._objectCount)
self.assertCommandEqual("destroy", key=self.commander.key)
Loading