Add a test suite and some basic navigation
This commit is contained in:
33
.coveragerc
Normal file
33
.coveragerc
Normal file
@@ -0,0 +1,33 @@
|
||||
[run]
|
||||
branch = True
|
||||
parallel = True
|
||||
source = .
|
||||
omit =
|
||||
.tox/*
|
||||
/usr/*
|
||||
setup.py
|
||||
# Don't complain if non-runnable code isn't run
|
||||
*/__main__.py
|
||||
|
||||
[report]
|
||||
show_missing = True
|
||||
skip_covered = True
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
\#\s*pragma: no cover
|
||||
# We optionally substitute this
|
||||
${COVERAGE_IGNORE_WINDOWS}
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
^\s*raise AssertionError\b
|
||||
^\s*raise NotImplementedError\b
|
||||
^\s*return NotImplemented\b
|
||||
^\s*raise$
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
^if __name__ == ['"]__main__['"]:$
|
||||
|
||||
[html]
|
||||
directory = coverage-html
|
||||
|
||||
# vim:ft=dosini
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1 +1,8 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
/.coverage
|
||||
/.coverage.*
|
||||
/.mypy_cache
|
||||
/.pytest_cache
|
||||
/.tox
|
||||
/venv*
|
||||
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2019 Anthony Sottile
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
9
README.md
Normal file
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
babi
|
||||
====
|
||||
|
||||
a text editor, eventually...
|
||||
|
||||
### why is it called babi?
|
||||
|
||||
I usually use the text editor `nano`, frequently I typo this. on a qwerty
|
||||
keyboard, when the right hand is shifted left by one, `nano` becomes `babi`.
|
||||
66
babi.py
66
babi.py
@@ -4,6 +4,8 @@ import curses
|
||||
from typing import Dict
|
||||
from typing import Tuple
|
||||
|
||||
VERSION_STR = 'babi v0'
|
||||
|
||||
|
||||
def _get_color_pair_mapping() -> Dict[Tuple[int, int], int]:
|
||||
ret = {}
|
||||
@@ -63,18 +65,78 @@ def _color_test(stdscr: '_curses._CursesWindow') -> None:
|
||||
x += 4
|
||||
y += 1
|
||||
x = 0
|
||||
stdscr.get_wch()
|
||||
|
||||
|
||||
def _write_header(
|
||||
stdscr: '_curses._CursesWindow',
|
||||
filename: str,
|
||||
*,
|
||||
modified: bool,
|
||||
) -> None:
|
||||
filename = filename or '<<new file>>'
|
||||
if modified:
|
||||
filename += ' *'
|
||||
centered_filename = filename.center(curses.COLS)[len(VERSION_STR) + 2:]
|
||||
s = f' {VERSION_STR} {centered_filename}'
|
||||
stdscr.addstr(0, 0, s, curses.A_REVERSE)
|
||||
|
||||
|
||||
def _write_status(stdscr: '_curses._CursesWindow', status: str) -> None:
|
||||
stdscr.addstr(curses.LINES - 1, 0, ' ' * (curses.COLS - 1))
|
||||
if status:
|
||||
status = f' {status} '
|
||||
offset = (curses.COLS - len(status)) // 2
|
||||
stdscr.addstr(curses.LINES - 1, offset, status, curses.A_REVERSE)
|
||||
|
||||
|
||||
def c_main(stdscr: '_curses._CursesWindow', args: argparse.Namespace) -> None:
|
||||
_init_colors(stdscr)
|
||||
|
||||
if args.color_test:
|
||||
_color_test(stdscr)
|
||||
stdscr.getch()
|
||||
return _color_test(stdscr)
|
||||
|
||||
filename = args.filename
|
||||
status = ''
|
||||
status_action_counter = -1
|
||||
position_y, position_x = 0, 0
|
||||
|
||||
def _set_status(s: str) -> None:
|
||||
nonlocal status, status_action_counter
|
||||
status = s
|
||||
status_action_counter = 25
|
||||
|
||||
while True:
|
||||
if status_action_counter == 0:
|
||||
status = ''
|
||||
status_action_counter -= 1
|
||||
|
||||
_write_header(stdscr, filename, modified=False)
|
||||
_write_status(stdscr, status)
|
||||
stdscr.move(position_y + 1, position_x)
|
||||
|
||||
wch = stdscr.get_wch()
|
||||
key = wch if isinstance(wch, int) else ord(wch)
|
||||
keyname = curses.keyname(key)
|
||||
|
||||
if key == curses.KEY_DOWN:
|
||||
position_y = min(position_y + 1, curses.LINES - 2)
|
||||
elif key == curses.KEY_UP:
|
||||
position_y = max(position_y - 1, 0)
|
||||
elif key == curses.KEY_RIGHT:
|
||||
position_x = min(position_x + 1, curses.COLS - 1)
|
||||
elif key == curses.KEY_LEFT:
|
||||
position_x = max(position_x - 1, 0)
|
||||
elif keyname == b'^X':
|
||||
return
|
||||
else:
|
||||
_set_status(f'unknown key: {keyname} ({key})')
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--color-test', action='store_true')
|
||||
parser.add_argument('filename', nargs='?')
|
||||
args = parser.parse_args()
|
||||
curses.wrapper(c_main, args)
|
||||
return 0
|
||||
|
||||
4
requirements-dev.txt
Normal file
4
requirements-dev.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
coverage
|
||||
git+https://github.com/ionelmc/python-remote-pdb@a5469c2d
|
||||
git+https://github.com/mjsir911/hecate@092f811
|
||||
pytest
|
||||
43
setup.cfg
Normal file
43
setup.cfg
Normal file
@@ -0,0 +1,43 @@
|
||||
[metadata]
|
||||
name = babi
|
||||
version = 0.0.0
|
||||
description = a text editor
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/asottile/babi
|
||||
author = Anthony Sottile
|
||||
author_email = asottile@umich.edu
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
classifiers =
|
||||
License :: OSI Approved :: MIT License
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
Programming Language :: Python :: Implementation :: PyPy
|
||||
|
||||
[options]
|
||||
py_modules = babi
|
||||
python_requires = >=3.6
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
babi = babi:main
|
||||
|
||||
[bdist_wheel]
|
||||
universal = True
|
||||
|
||||
[mypy]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
|
||||
[mypy-testing.*]
|
||||
disallow_untyped_defs = false
|
||||
|
||||
[mypy-tests.*]
|
||||
disallow_untyped_defs = false
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
53
tests/babi_test.py
Normal file
53
tests/babi_test.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import contextlib
|
||||
import sys
|
||||
|
||||
from hecate import Runner
|
||||
|
||||
import babi
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def run(*args, **kwargs):
|
||||
cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', *args)
|
||||
with Runner(*cmd, **kwargs) as h:
|
||||
h.await_text(babi.VERSION_STR)
|
||||
yield h
|
||||
|
||||
|
||||
def await_text_missing(h, text):
|
||||
"""largely based on await_text"""
|
||||
for _ in h.poll_until_timeout():
|
||||
screen = h.screenshot()
|
||||
munged = screen.replace('\n', '')
|
||||
if text not in munged: # pragma: no branch
|
||||
return
|
||||
raise AssertionError(f'Timeout while waiting for text {text!r} to appear')
|
||||
|
||||
|
||||
def test_window_bounds(tmpdir):
|
||||
f = tmpdir.join('f.txt')
|
||||
f.write(f'{"x" * 40}\n' * 40)
|
||||
|
||||
with run(str(f), width=30, height=30) as h:
|
||||
# make sure we don't go off the top left of the screen
|
||||
h.press('LEFT')
|
||||
h.press('UP')
|
||||
# make sure we don't go off the bottom of the screen
|
||||
for i in range(32):
|
||||
h.press('RIGHT')
|
||||
h.press('DOWN')
|
||||
h.press('C-x')
|
||||
h.await_exit()
|
||||
|
||||
|
||||
def test_status_clearing_behaviour():
|
||||
with run() as h:
|
||||
h.press('C-j')
|
||||
h.await_text('unknown key')
|
||||
for i in range(24):
|
||||
h.press('LEFT')
|
||||
h.await_text('unknown key')
|
||||
h.press('LEFT')
|
||||
await_text_missing(h, 'unknown key')
|
||||
h.press('C-x')
|
||||
h.await_exit()
|
||||
15
tox.ini
Normal file
15
tox.ini
Normal file
@@ -0,0 +1,15 @@
|
||||
[tox]
|
||||
envlist = pypy3,py36,py37,pre-commit
|
||||
|
||||
[testenv]
|
||||
deps = -rrequirements-dev.txt
|
||||
commands =
|
||||
coverage erase
|
||||
coverage run -m pytest {posargs:tests}
|
||||
coverage combine
|
||||
coverage report
|
||||
|
||||
[testenv:pre-commit]
|
||||
skip_install = true
|
||||
deps = pre-commit
|
||||
commands = pre-commit run --all-files --show-diff-on-failure
|
||||
Reference in New Issue
Block a user