Add a test suite and some basic navigation

This commit is contained in:
Anthony Sottile
2019-07-27 19:22:43 -07:00
parent 13a6bbc51b
commit c52702d0a6
11 changed files with 249 additions and 2 deletions

33
.coveragerc Normal file
View 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
View File

@@ -1 +1,8 @@
*.egg-info
*.pyc
/.coverage
/.coverage.*
/.mypy_cache
/.pytest_cache
/.tox
/venv*

19
LICENSE Normal file
View 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
View 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
View File

@@ -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
View 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
View 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

2
setup.py Normal file
View File

@@ -0,0 +1,2 @@
from setuptools import setup
setup()

0
tests/__init__.py Normal file
View File

53
tests/babi_test.py Normal file
View 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
View 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