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
|
/.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 Dict
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
VERSION_STR = 'babi v0'
|
||||||
|
|
||||||
|
|
||||||
def _get_color_pair_mapping() -> Dict[Tuple[int, int], int]:
|
def _get_color_pair_mapping() -> Dict[Tuple[int, int], int]:
|
||||||
ret = {}
|
ret = {}
|
||||||
@@ -63,18 +65,78 @@ def _color_test(stdscr: '_curses._CursesWindow') -> None:
|
|||||||
x += 4
|
x += 4
|
||||||
y += 1
|
y += 1
|
||||||
x = 0
|
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:
|
def c_main(stdscr: '_curses._CursesWindow', args: argparse.Namespace) -> None:
|
||||||
_init_colors(stdscr)
|
_init_colors(stdscr)
|
||||||
|
|
||||||
if args.color_test:
|
if args.color_test:
|
||||||
_color_test(stdscr)
|
return _color_test(stdscr)
|
||||||
stdscr.getch()
|
|
||||||
|
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:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--color-test', action='store_true')
|
parser.add_argument('--color-test', action='store_true')
|
||||||
|
parser.add_argument('filename', nargs='?')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
curses.wrapper(c_main, args)
|
curses.wrapper(c_main, args)
|
||||||
return 0
|
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