From ccbe8b8d13ebdad09d282da51d118670a566cba5 Mon Sep 17 00:00:00 2001 From: hut Date: Tue, 4 May 2010 23:29:54 +0200 Subject: attempt to fix utf issues (wrong calculation of width) --- test/tc_utfwidth.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 test/tc_utfwidth.py (limited to 'test') diff --git a/test/tc_utfwidth.py b/test/tc_utfwidth.py new file mode 100644 index 00000000..cf564990 --- /dev/null +++ b/test/tc_utfwidth.py @@ -0,0 +1,42 @@ +# -*- encoding: utf8 -*- +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +if __name__ == '__main__': from __init__ import init; init() + +from unittest import TestCase, main +from ranger.ext.utfwidth import * + +a_ascii = "a" # width = 1, bytes = 1 +a_umlaut = "ä" # width = 1, bytes = 2 +a_katakana = "ア" # width = 2, bytes = 3 +# need one with width = 1 & bytes = 3 + +class Test(TestCase): + def test_utf_byte_length(self): + self.assertEqual(1, utf_byte_length(a_ascii[0])) + self.assertEqual(2, utf_byte_length(a_umlaut[0])) + self.assertEqual(3, utf_byte_length(a_katakana[0])) + + def test_uwid(self): + self.assertEqual(1, uwid(a_ascii)) + self.assertEqual(1, uwid(a_umlaut)) + self.assertEqual(2, uwid(a_katakana)) + self.assertEqual(3, uwid(a_katakana + a_umlaut)) + self.assertEqual(4, uwid("asdf")) + self.assertEqual(5, uwid("löööl")) + self.assertEqual(6, uwid("バババ")) + +if __name__ == '__main__': main() -- cgit 1.4.1-2-gfad0 From 9e435dcd5bcaf20f74f979f5ac79714172648226 Mon Sep 17 00:00:00 2001 From: hut Date: Mon, 17 May 2010 17:28:34 +0200 Subject: fixed utf stuff --- ranger/ext/utfwidth.py | 60 +++++++++++++++++++++++++++++++++++-------- ranger/gui/widgets/console.py | 9 ++++--- test/tc_utfwidth.py | 6 ++--- 3 files changed, 58 insertions(+), 17 deletions(-) (limited to 'test') diff --git a/ranger/ext/utfwidth.py b/ranger/ext/utfwidth.py index 2881a2a0..31440ef0 100644 --- a/ranger/ext/utfwidth.py +++ b/ranger/ext/utfwidth.py @@ -24,25 +24,61 @@ WIDE = 2 def utf_byte_length(string): """Return the byte length of one utf character""" firstord = ord(string[0]) - if firstord < 0x01111111: + if firstord < 0b01111111: return 1 - if firstord < 0x10111111: + if firstord < 0b10111111: return 1 # invalid - if firstord < 0x11011111: - return min(2, len(string)) - if firstord < 0x11101111: - return min(3, len(string)) - if firstord < 0x11110100: - return min(4, len(string)) + if firstord < 0b11011111: + return 2 + if firstord < 0b11101111: + return 3 + if firstord < 0b11110100: + return 4 return 1 # invalid def utf_char_width(string): - # XXX + """Return the width of a single character""" + # Inspired by cmus uchar.c u = _utf_char_to_int(string) if u < 0x1100: return NARROW - else: + # Hangul Jamo init. constonants + if u <= 0x115F: return WIDE + # Angle Brackets + if u == 0x2329 or u == 0x232A: + return WIDE + if u < 0x2e80: + return NARROW + # CJK ... Yi + if u < 0x302A: + return WIDE + if u <= 0x302F: + return NARROW + if u == 0x303F or u == 0x3099 or u == 0x309a: + return NARROW + # CJK ... Yi + if u <= 0xA4CF: + return WIDE + # Hangul Syllables + if u >= 0xAC00 and u <= 0xD7A3: + return WIDE + # CJK Compatibility Ideographs + if u >= 0xF900 and u <= 0xFAFF: + return WIDE + # CJK Compatibility Forms + if u >= 0xFE30 and u <= 0xFE6F: + return WIDE + # Fullwidth Forms + if u >= 0xFF00 and u <= 0xFF60 or u >= 0xFFE0 and u <= 0xFFE6: + return WIDE + # CJK Extra Stuff + if u >= 0x20000 and u <= 0x2FFFD: + return WIDE + # ? + if u >= 0x30000 and u <= 0x3FFFD: + return WIDE + return NARROW # invalid def _utf_char_to_int(string): # Squash the last 6 bits of each byte together to an integer @@ -52,16 +88,18 @@ def _utf_char_to_int(string): return u def uwid(string): + """Return the width of a string""" end = len(string) i = 0 width = 0 while i < end: bytelen = utf_byte_length(string[i:]) - width += 1 + width += utf_char_width(string[i:i+bytelen]) i += bytelen return width def uchars(string): + """Return a list with one string for each character""" end = len(string) i = 0 result = [] diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 5a538ce2..51ecf3b2 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -223,11 +223,14 @@ class Console(Widget): def move(self, **keywords): direction = Direction(keywords) if direction.horizontal(): - self.pos = direction.move( + uc = uchars(self.line) + upos = len(uchars(self.line[:self.pos])) + newupos = direction.move( direction=direction.right(), minimum=0, - maximum=len(self.line) + 1, - current=self.pos) + maximum=len(uc) + 1, + current=upos) + self.pos = len(''.join(uc[:newupos])) def delete_rest(self, direction): self.tab_deque = None diff --git a/test/tc_utfwidth.py b/test/tc_utfwidth.py index cf564990..d8ffbe1d 100644 --- a/test/tc_utfwidth.py +++ b/test/tc_utfwidth.py @@ -26,9 +26,9 @@ a_katakana = "ア" # width = 2, bytes = 3 class Test(TestCase): def test_utf_byte_length(self): - self.assertEqual(1, utf_byte_length(a_ascii[0])) - self.assertEqual(2, utf_byte_length(a_umlaut[0])) - self.assertEqual(3, utf_byte_length(a_katakana[0])) + self.assertEqual(1, utf_byte_length(a_ascii)) + self.assertEqual(2, utf_byte_length(a_umlaut)) + self.assertEqual(3, utf_byte_length(a_katakana)) def test_uwid(self): self.assertEqual(1, uwid(a_ascii)) -- cgit 1.4.1-2-gfad0 From 85fd52880db93c6cd01f2c459eb46e6dd35d777c Mon Sep 17 00:00:00 2001 From: hut Date: Mon, 7 Jun 2010 15:53:01 +0200 Subject: simplified all_tests.py, moved to test/ --- Makefile | 4 ++-- all_benchmarks.py | 58 -------------------------------------------------- all_tests.py | 36 ------------------------------- test/__init__.py | 52 -------------------------------------------- test/all_benchmarks.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/all_tests.py | 33 ++++++++++++++++++++++++++++ test/bm_loader.py | 2 +- test/ranger | 1 + test/tc_bookmarks.py | 2 -- test/tc_colorscheme.py | 2 -- test/tc_direction.py | 2 -- test/tc_directory.py | 2 -- test/tc_displayable.py | 4 +--- test/tc_ext.py | 1 - test/tc_history.py | 2 -- test/tc_keyapi.py | 2 -- test/tc_loader.py | 4 +--- test/tc_newkeys.py | 3 +-- test/tc_signal.py | 1 - test/tc_ui.py | 6 ++---- test/tc_utfwidth.py | 2 -- test/test.py | 20 ----------------- test/testlib.py | 43 +++++++++++++++++++++++++++++++++++++ 23 files changed, 143 insertions(+), 197 deletions(-) delete mode 100755 all_benchmarks.py delete mode 100755 all_tests.py create mode 100755 test/all_benchmarks.py create mode 100755 test/all_tests.py create mode 120000 test/ranger delete mode 100644 test/test.py create mode 100644 test/testlib.py (limited to 'test') diff --git a/Makefile b/Makefile index 6a103a02..a5589803 100644 --- a/Makefile +++ b/Makefile @@ -100,10 +100,10 @@ cleandoc: test -d $(DOCDIR) && rm -f -- $(DOCDIR)/*.html test: - @$(PYTHON) all_tests.py 1 + @$(PYTHON) test/all_tests.py 1 bm: - @$(PYTHON) all_benchmarks.py $(BMCOUNT) + @$(PYTHON) test/all_benchmarks.py $(BMCOUNT) snapshot: git archive HEAD | gzip > $(NAME)-$(VERSION)-$(shell git rev-parse HEAD | cut -b 1-8).tar.gz diff --git a/all_benchmarks.py b/all_benchmarks.py deleted file mode 100755 index 73316658..00000000 --- a/all_benchmarks.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/python -# Copyright (C) 2009, 2010 Roman Zimbelmann -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Run all the tests inside the test/ directory as a test suite.""" -if __name__ == '__main__': - from re import compile - from test import * - from time import time - from types import FunctionType as function - from sys import argv - bms = [] - try: - n = int(argv[1]) - except IndexError: - n = 10 - if len(argv) > 2: - args = [compile(re) for re in argv[2:]] - def allow(name): - for re in args: - if re.search(name): - return True - else: - return False - else: - allow = lambda name: True - for key, val in vars().copy().items(): - if key.startswith('bm_'): - bms.extend(v for k,v in vars(val).items() if type(v) == type) - for bmclass in bms: - for attrname in vars(bmclass): - if not attrname.startswith('bm_'): - continue - bmobj = bmclass() - t1 = time() - method = getattr(bmobj, attrname) - methodname = "{0}.{1}".format(bmobj.__class__.__name__, method.__name__) - if allow(methodname): - try: - method(n) - except: - print("{0} failed!".format(methodname)) - raise - else: - t2 = time() - print("{0:60}: {1:10}s".format(methodname, t2 - t1)) diff --git a/all_tests.py b/all_tests.py deleted file mode 100755 index 90926918..00000000 --- a/all_tests.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python -# Copyright (C) 2009, 2010 Roman Zimbelmann -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Run all the tests inside the test/ directory as a test suite.""" -if __name__ == '__main__': - import unittest - from test import * - from sys import exit, argv - - try: - verbosity = int(argv[1]) - except IndexError: - verbosity = 2 - - tests = [] - for key, val in vars().copy().items(): - if key.startswith('tc_'): - tests.extend(v for k,v in vars(val).items() if type(v) == type) - - suite = unittest.TestSuite(map(unittest.makeSuite, tests)) - result = unittest.TextTestRunner(verbosity=verbosity).run(suite) - if len(result.errors) + len(result.failures) > 0: - exit(1) diff --git a/test/__init__.py b/test/__init__.py index d87d1fc2..e69de29b 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,52 +0,0 @@ -# Copyright (C) 2009, 2010 Roman Zimbelmann -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os, sys - -__all__ = [ x[0:x.index('.')] \ - for x in os.listdir(os.path.dirname(__file__)) \ - if x.startswith('tc_') or x.startswith('bm_')] - -def TODO(fnc): - def result(*arg, **kw): - try: - fnc(*arg, **kw) - except: - pass # failure expected - return result - -def init(): - sys.path.append(os.path.abspath(os.path.join(sys.path[0], '..'))) - -class Fake(object): - def __getattr__(self, attrname): - val = Fake() - self.__dict__[attrname] = val - return val - - def __call__(self, *_, **__): - return Fake() - - def __clear__(self): - self.__dict__.clear() - - def __iter__(self): - return iter(()) - -class OK(Exception): - pass - -def raise_ok(*_, **__): - raise OK() diff --git a/test/all_benchmarks.py b/test/all_benchmarks.py new file mode 100755 index 00000000..73316658 --- /dev/null +++ b/test/all_benchmarks.py @@ -0,0 +1,58 @@ +#!/usr/bin/python +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Run all the tests inside the test/ directory as a test suite.""" +if __name__ == '__main__': + from re import compile + from test import * + from time import time + from types import FunctionType as function + from sys import argv + bms = [] + try: + n = int(argv[1]) + except IndexError: + n = 10 + if len(argv) > 2: + args = [compile(re) for re in argv[2:]] + def allow(name): + for re in args: + if re.search(name): + return True + else: + return False + else: + allow = lambda name: True + for key, val in vars().copy().items(): + if key.startswith('bm_'): + bms.extend(v for k,v in vars(val).items() if type(v) == type) + for bmclass in bms: + for attrname in vars(bmclass): + if not attrname.startswith('bm_'): + continue + bmobj = bmclass() + t1 = time() + method = getattr(bmobj, attrname) + methodname = "{0}.{1}".format(bmobj.__class__.__name__, method.__name__) + if allow(methodname): + try: + method(n) + except: + print("{0} failed!".format(methodname)) + raise + else: + t2 = time() + print("{0:60}: {1:10}s".format(methodname, t2 - t1)) diff --git a/test/all_tests.py b/test/all_tests.py new file mode 100755 index 00000000..dff49b60 --- /dev/null +++ b/test/all_tests.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Run all the tests inside the test/ directory as a test suite.""" +if __name__ == '__main__': + import unittest + import sys + import os + + try: + verbosity = int(sys.argv[1]) + except IndexError: + verbosity = 2 + + ls = os.listdir(sys.path[0]) + paths = [p[:-3] for p in ls if p[:3] == 'tc_' and p[-3:] == '.py'] + suite = unittest.TestLoader().loadTestsFromNames(paths) + result = unittest.TextTestRunner(verbosity=verbosity).run(suite) + if len(result.errors) + len(result.failures) > 0: + sys.exit(1) diff --git a/test/bm_loader.py b/test/bm_loader.py index 745e6f3b..968640a5 100644 --- a/test/bm_loader.py +++ b/test/bm_loader.py @@ -18,7 +18,7 @@ from ranger.fsobject import Directory, File from ranger.ext.openstruct import OpenStruct import os.path from ranger.shared import FileManagerAware, SettingsAware -from test import Fake +from testlib import Fake from os.path import realpath, join, dirname from subprocess import Popen, PIPE TESTDIR = realpath(join(dirname(__file__), '/usr/include')) diff --git a/test/ranger b/test/ranger new file mode 120000 index 00000000..5459458c --- /dev/null +++ b/test/ranger @@ -0,0 +1 @@ +../ranger \ No newline at end of file diff --git a/test/tc_bookmarks.py b/test/tc_bookmarks.py index f45ba061..9b41f1c6 100644 --- a/test/tc_bookmarks.py +++ b/test/tc_bookmarks.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - from os.path import realpath, join, dirname import unittest import os diff --git a/test/tc_colorscheme.py b/test/tc_colorscheme.py index dbaac1f9..8d6adee6 100644 --- a/test/tc_colorscheme.py +++ b/test/tc_colorscheme.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - from unittest import TestCase, main import random import ranger.colorschemes diff --git a/test/tc_direction.py b/test/tc_direction.py index f45b4b36..5c1776cf 100644 --- a/test/tc_direction.py +++ b/test/tc_direction.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - import unittest from ranger.ext.direction import Direction from ranger.ext.openstruct import OpenStruct diff --git a/test/tc_directory.py b/test/tc_directory.py index 024ebc9d..a702c4db 100644 --- a/test/tc_directory.py +++ b/test/tc_directory.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - import sys from os.path import realpath, join, dirname diff --git a/test/tc_displayable.py b/test/tc_displayable.py index 50f37845..1c66a40e 100644 --- a/test/tc_displayable.py +++ b/test/tc_displayable.py @@ -13,14 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - import unittest import curses from random import randint from ranger.gui.displayable import Displayable, DisplayableContainer -from test import Fake, OK, raise_ok, TODO +from testlib import Fake, OK, raise_ok, TODO class TestWithFakeCurses(unittest.TestCase): def setUp(self): diff --git a/test/tc_ext.py b/test/tc_ext.py index b8094233..919f35a2 100644 --- a/test/tc_ext.py +++ b/test/tc_ext.py @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() import unittest from collections import deque diff --git a/test/tc_history.py b/test/tc_history.py index d027231a..33784e14 100644 --- a/test/tc_history.py +++ b/test/tc_history.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - from ranger.container import History from unittest import TestCase, main import unittest diff --git a/test/tc_keyapi.py b/test/tc_keyapi.py index 2f522173..48282a7d 100644 --- a/test/tc_keyapi.py +++ b/test/tc_keyapi.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - from unittest import TestCase, main class Test(TestCase): diff --git a/test/tc_loader.py b/test/tc_loader.py index 53ac5617..9f7f7322 100644 --- a/test/tc_loader.py +++ b/test/tc_loader.py @@ -13,13 +13,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - import unittest import os from os.path import realpath, join, dirname -from test import Fake +from testlib import Fake from ranger.shared import FileManagerAware, SettingsAware from ranger.core.loader import Loader from ranger.fsobject import Directory, File diff --git a/test/tc_newkeys.py b/test/tc_newkeys.py index c7a33025..fd856f17 100644 --- a/test/tc_newkeys.py +++ b/test/tc_newkeys.py @@ -14,10 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() from unittest import TestCase, main -from test import TODO +from testlib import TODO from ranger.ext.tree import Tree from ranger.container.keymap import * from ranger.container.keybuffer import KeyBuffer diff --git a/test/tc_signal.py b/test/tc_signal.py index 35b4eebe..f31681f4 100644 --- a/test/tc_signal.py +++ b/test/tc_signal.py @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() import unittest import gc from ranger.ext.signal_dispatcher import * diff --git a/test/tc_ui.py b/test/tc_ui.py index 3c659459..dc8d669d 100644 --- a/test/tc_ui.py +++ b/test/tc_ui.py @@ -13,14 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - import unittest import curses from ranger.gui import ui -from test import Fake, OK, raise_ok +from testlib import Fake, OK, raise_ok ui.curses = Fake() @@ -39,7 +37,7 @@ class Test(unittest.TestCase): def tearDown(self): self.ui.destroy() - + def test_passing(self): # Test whether certain method calls are passed to widgets widget = self.ui.widget diff --git a/test/tc_utfwidth.py b/test/tc_utfwidth.py index d8ffbe1d..2aa5fa6d 100644 --- a/test/tc_utfwidth.py +++ b/test/tc_utfwidth.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -if __name__ == '__main__': from __init__ import init; init() - from unittest import TestCase, main from ranger.ext.utfwidth import * diff --git a/test/test.py b/test/test.py deleted file mode 100644 index d0a69e5a..00000000 --- a/test/test.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2009, 2010 Roman Zimbelmann -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Workaround to allow running single test cases directly""" -try: - from __init__ import init, Fake, OK, raise_ok, TODO -except: - from test import init, Fake, OK, raise_ok, TODO diff --git a/test/testlib.py b/test/testlib.py new file mode 100644 index 00000000..29dd9e07 --- /dev/null +++ b/test/testlib.py @@ -0,0 +1,43 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +def TODO(fnc): + def result(*arg, **kw): + try: + fnc(*arg, **kw) + except: + pass # failure expected + return result + +class Fake(object): + def __getattr__(self, attrname): + val = Fake() + self.__dict__[attrname] = val + return val + + def __call__(self, *_, **__): + return Fake() + + def __clear__(self): + self.__dict__.clear() + + def __iter__(self): + return iter(()) + +class OK(Exception): + pass + +def raise_ok(*_, **__): + raise OK() -- cgit 1.4.1-2-gfad0 From 8129ccb6d1e10182d60a71483da2c3db08108adb Mon Sep 17 00:00:00 2001 From: hut Date: Wed, 9 Jun 2010 10:33:50 +0200 Subject: Changed hashbang line to "#!/usr/bin/env python" --- INSTALL | 2 +- doc/print_colors.py | 2 +- doc/print_keys.py | 2 +- ranger.py | 2 +- ranger/__main__.py | 2 +- test/all_benchmarks.py | 2 +- test/all_tests.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) (limited to 'test') diff --git a/INSTALL b/INSTALL index 5fc5ca6b..9939c11b 100644 --- a/INSTALL +++ b/INSTALL @@ -21,7 +21,7 @@ though you might want to read the Makefile first) 0. Make sure you have a recent version of python, including the curses module, which is the case if this shell command prints no errors: - python -c 'import curses' + python -c 'import curses, sys; assert sys.version >= "2.6"' 1. Copy the file "ranger.py" into any of the directories in the PATH diff --git a/doc/print_colors.py b/doc/print_colors.py index 7ffd6500..c3508fa6 100755 --- a/doc/print_colors.py +++ b/doc/print_colors.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python """ You can use this tool to display all supported colors and their color number. It will exit after a keypress. diff --git a/doc/print_keys.py b/doc/print_keys.py index 0790acab..f87a2a40 100644 --- a/doc/print_keys.py +++ b/doc/print_keys.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python """ You can use this tool to find out values of keypresses """ diff --git a/ranger.py b/ranger.py index aca1b557..a3f2095b 100755 --- a/ranger.py +++ b/ranger.py @@ -1,4 +1,4 @@ -#!/usr/bin/python -O +#!/usr/bin/env python -O # coding=utf-8 # # Ranger: Explore your forest of files from inside your terminal diff --git a/ranger/__main__.py b/ranger/__main__.py index 0c38b6e1..2164c045 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # coding=utf-8 # # Copyright (C) 2009, 2010 Roman Zimbelmann diff --git a/test/all_benchmarks.py b/test/all_benchmarks.py index 73316658..84d6817d 100755 --- a/test/all_benchmarks.py +++ b/test/all_benchmarks.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Copyright (C) 2009, 2010 Roman Zimbelmann # # This program is free software: you can redistribute it and/or modify diff --git a/test/all_tests.py b/test/all_tests.py index dff49b60..04321462 100755 --- a/test/all_tests.py +++ b/test/all_tests.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Copyright (C) 2009, 2010 Roman Zimbelmann # # This program is free software: you can redistribute it and/or modify -- cgit 1.4.1-2-gfad0 From 244fc3497a6902298d10ac82be9c6e0cb68c2011 Mon Sep 17 00:00:00 2001 From: hut Date: Wed, 9 Jun 2010 11:01:34 +0200 Subject: all_tests.py: cleanup --- test/all_tests.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'test') diff --git a/test/all_tests.py b/test/all_tests.py index 04321462..7cfc855f 100755 --- a/test/all_tests.py +++ b/test/all_tests.py @@ -14,20 +14,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Run all the tests inside the test/ directory as a test suite.""" -if __name__ == '__main__': - import unittest - import sys - import os +""" +Run all the tests inside this directory as a test suite. +Usage: ./all_tests.py [verbosity] +""" - try: - verbosity = int(sys.argv[1]) - except IndexError: - verbosity = 2 +import os +import sys +import unittest - ls = os.listdir(sys.path[0]) - paths = [p[:-3] for p in ls if p[:3] == 'tc_' and p[-3:] == '.py'] - suite = unittest.TestLoader().loadTestsFromNames(paths) - result = unittest.TextTestRunner(verbosity=verbosity).run(suite) - if len(result.errors) + len(result.failures) > 0: +if __name__ == '__main__': + verbosity = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + tests = (fname[:-3] for fname in os.listdir(sys.path[0]) \ + if fname[:3] == 'tc_' and fname[-3:] == '.py') + suite = unittest.TestLoader().loadTestsFromNames(tests) + result = unittest.TextTestRunner(verbosity=verbosity).run(suite) + if len(result.errors + result.failures) > 0: sys.exit(1) -- cgit 1.4.1-2-gfad0 From 110dd383d250c20b1bf74f059f8f996d81a6d123 Mon Sep 17 00:00:00 2001 From: hut Date: Wed, 9 Jun 2010 11:43:36 +0200 Subject: all_benchmarks.py: fix + cleanup --- test/all_benchmarks.py | 75 +++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 40 deletions(-) (limited to 'test') diff --git a/test/all_benchmarks.py b/test/all_benchmarks.py index 84d6817d..e1b47142 100755 --- a/test/all_benchmarks.py +++ b/test/all_benchmarks.py @@ -14,45 +14,40 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Run all the tests inside the test/ directory as a test suite.""" +""" +Run all the benchmarks inside this directory. +Usage: ./all_benchmarks.py [count] [regexp-filters...] +""" + +import os +import re +import sys +import time + if __name__ == '__main__': - from re import compile - from test import * - from time import time - from types import FunctionType as function - from sys import argv - bms = [] - try: - n = int(argv[1]) - except IndexError: - n = 10 - if len(argv) > 2: - args = [compile(re) for re in argv[2:]] - def allow(name): - for re in args: - if re.search(name): - return True + count = int(sys.argv[1]) if len(sys.argv) > 1 else 10 + regexes = [re.compile(fltr) for fltr in sys.argv[2:]] + modules = (fname[:-3] for fname in os.listdir(sys.path[0]) \ + if fname[:3] == 'bm_' and fname[-3:] == '.py') + + benchmarks = [] # find all benchmark (class, methodname) pairs + for val in [__import__(module) for module in modules]: + for cls in vars(val).values(): + if type(cls) == type: + for methodname in vars(cls): + if methodname.startswith('bm_'): + benchmarks.append((cls, methodname)) + + for cls, methodname in benchmarks: + full_method_name = "{0}.{1}".format(cls.__name__, methodname) + if all(re.search(full_method_name) for re in regexes): + method = getattr(cls(), methodname) + t1 = time.time() + try: + method(count) + except: + print("{0} failed!".format(full_method_name)) + raise else: - return False - else: - allow = lambda name: True - for key, val in vars().copy().items(): - if key.startswith('bm_'): - bms.extend(v for k,v in vars(val).items() if type(v) == type) - for bmclass in bms: - for attrname in vars(bmclass): - if not attrname.startswith('bm_'): - continue - bmobj = bmclass() - t1 = time() - method = getattr(bmobj, attrname) - methodname = "{0}.{1}".format(bmobj.__class__.__name__, method.__name__) - if allow(methodname): - try: - method(n) - except: - print("{0} failed!".format(methodname)) - raise - else: - t2 = time() - print("{0:60}: {1:10}s".format(methodname, t2 - t1)) + t2 = time.time() + print("{0:60}: {1:10}s".format(full_method_name, t2 - t1)) -- cgit 1.4.1-2-gfad0 From 26962ded19264ae1885386783c861d002d8fd1dc Mon Sep 17 00:00:00 2001 From: hut Date: Fri, 18 Jun 2010 11:44:53 +0200 Subject: all_benchmarks: shortened --- test/all_benchmarks.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) (limited to 'test') diff --git a/test/all_benchmarks.py b/test/all_benchmarks.py index e1b47142..20f11ad8 100755 --- a/test/all_benchmarks.py +++ b/test/all_benchmarks.py @@ -30,15 +30,7 @@ if __name__ == '__main__': modules = (fname[:-3] for fname in os.listdir(sys.path[0]) \ if fname[:3] == 'bm_' and fname[-3:] == '.py') - benchmarks = [] # find all benchmark (class, methodname) pairs - for val in [__import__(module) for module in modules]: - for cls in vars(val).values(): - if type(cls) == type: - for methodname in vars(cls): - if methodname.startswith('bm_'): - benchmarks.append((cls, methodname)) - - for cls, methodname in benchmarks: + def run_benchmark(cls, methodname): full_method_name = "{0}.{1}".format(cls.__name__, methodname) if all(re.search(full_method_name) for re in regexes): method = getattr(cls(), methodname) @@ -51,3 +43,10 @@ if __name__ == '__main__': else: t2 = time.time() print("{0:60}: {1:10}s".format(full_method_name, t2 - t1)) + + for val in [__import__(module) for module in modules]: + for cls in vars(val).values(): + if type(cls) == type: + for methodname in vars(cls): + if methodname.startswith('bm_'): + run_benchmark(cls, methodname) -- cgit 1.4.1-2-gfad0 From 7bc8b3fc32b44a8db8bfb321423a1bb7718350ab Mon Sep 17 00:00:00 2001 From: hut Date: Thu, 24 Jun 2010 22:41:20 +0200 Subject: ext.human_readable: more efficient implementation plus unit tests and benchmark. --- ranger/ext/human_readable.py | 53 ++++++++++++++++++++++++++++---------------- test/bm_human_readable.py | 45 +++++++++++++++++++++++++++++++++++++ test/tc_human_readable.py | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 test/bm_human_readable.py create mode 100644 test/tc_human_readable.py (limited to 'test') diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index beeaf6d3..35dbc35e 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -13,24 +13,39 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import math - -ONE_KB = 1024 -UNITS = 'BKMGTP' -MAX_EXPONENT = len(UNITS) - 1 - def human_readable(byte, seperator=' '): - if not byte: - return '0' - - exponent = int(math.log(byte, 2) / 10) - flt = round(float(byte) / (1 << (10 * exponent)), 2) + """ + Convert a large number of bytes to an easily readable format. - if exponent > MAX_EXPONENT: - return '>9000' # off scale - - if int(flt) == flt: - return '%.0f%s%s' % (flt, seperator, UNITS[exponent]) - - else: - return '%.2f%s%s' % (flt, seperator, UNITS[exponent]) + >>> human_readable(54) + "54 B" + >>> human_readable(1500) + "1.46 K" + >>> human_readable(2 ** 20 * 1023) + "1023 M" + """ + if byte <= 0: + return '0' + if byte < 2**10: + return '%d%sB' % (byte, seperator) + if byte < 2**10 * 1000: + return '%.3g%sK' % (byte / 2**10.0, seperator) + if byte < 2**20: + return '%.4g%sK' % (byte / 2**10.0, seperator) + if byte < 2**20 * 1000: + return '%.3g%sM' % (byte / 2**20.0, seperator) + if byte < 2**30: + return '%.4g%sM' % (byte / 2**20.0, seperator) + if byte < 2**30 * 1000: + return '%.3g%sG' % (byte / 2**30.0, seperator) + if byte < 2**40: + return '%.4g%sG' % (byte / 2**30.0, seperator) + if byte < 2**40 * 1000: + return '%.3g%sT' % (byte / 2**40.0, seperator) + if byte < 2**50: + return '%.4g%sT' % (byte / 2**40.0, seperator) + if byte < 2**50 * 1000: + return '%.3g%sP' % (byte / 2**50.0, seperator) + if byte < 2**60: + return '%.4g%sP' % (byte / 2**50.0, seperator) + return '>9000' diff --git a/test/bm_human_readable.py b/test/bm_human_readable.py new file mode 100644 index 00000000..83f2a057 --- /dev/null +++ b/test/bm_human_readable.py @@ -0,0 +1,45 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from ranger.ext.human_readable import * + +# The version before 2010/06/24: +import math +UNITS = 'BKMGTP' +MAX_EXPONENT = len(UNITS) - 1 +def human_readable_old(byte, seperator=' '): + if not byte: + return '0' + + exponent = int(math.log(byte, 2) / 10) + flt = round(float(byte) / (1 << (10 * exponent)), 2) + + if exponent > MAX_EXPONENT: + return '>9000' # off scale + + if int(flt) == flt: + return '%.0f%s%s' % (flt, seperator, UNITS[exponent]) + + else: + return '%.2f%s%s' % (flt, seperator, UNITS[exponent]) + +class benchmark_human_readable(object): + def bm_current(self, n): + for i in range(n): + human_readable((128 * i) % 2**50) + + def bm_old(self, n): + for i in range(n): + human_readable_old((128 * i) % 2**50) diff --git a/test/tc_human_readable.py b/test/tc_human_readable.py new file mode 100644 index 00000000..50fc80a1 --- /dev/null +++ b/test/tc_human_readable.py @@ -0,0 +1,41 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest +from ranger.ext.human_readable import human_readable as hr + +class HumanReadableTest(unittest.TestCase): + def test_basic(self): + self.assertEqual("0", hr(0)) + self.assertEqual("1 B", hr(1)) + self.assertEqual("1 K", hr(2 ** 10)) + self.assertEqual("1 M", hr(2 ** 20)) + self.assertEqual("1 G", hr(2 ** 30)) + self.assertEqual(">9000", hr(2 ** 100)) + + def test_big(self): + self.assertEqual("1023 G", hr(2 ** 30 * 1023)) + self.assertEqual("1024 G", hr(2 ** 40 - 1)) + self.assertEqual("1 T", hr(2 ** 40)) + + def test_small(self): + self.assertEqual("1000 B", hr(1000)) + self.assertEqual("1.66 M", hr(1.66 * 2 ** 20)) + self.assertEqual("1.46 K", hr(1500)) + self.assertEqual("1.5 K", hr(2 ** 10 + 2 ** 9)) + self.assertEqual("1.5 K", hr(2 ** 10 + 2 ** 9 - 1)) + +if __name__ == '__main__': + unittest.main() -- cgit 1.4.1-2-gfad0 From 40763055c6c1995271acffe6ba5928893940f88b Mon Sep 17 00:00:00 2001 From: hut Date: Fri, 25 Jun 2010 14:57:12 +0200 Subject: tc_human_readable: additional testcase (which fails) --- test/tc_human_readable.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'test') diff --git a/test/tc_human_readable.py b/test/tc_human_readable.py index 50fc80a1..b931ba21 100644 --- a/test/tc_human_readable.py +++ b/test/tc_human_readable.py @@ -37,5 +37,9 @@ class HumanReadableTest(unittest.TestCase): self.assertEqual("1.5 K", hr(2 ** 10 + 2 ** 9)) self.assertEqual("1.5 K", hr(2 ** 10 + 2 ** 9 - 1)) + def test_no_exponent(self): + for i in range(2 ** 10, 2 ** 20, 512): + self.assertTrue('e' not in hr(i), "%d => %s" % (i, hr(i))) + if __name__ == '__main__': unittest.main() -- cgit 1.4.1-2-gfad0 From 0a8001b2a141b141e6e34b1ebcce316288ef7ab7 Mon Sep 17 00:00:00 2001 From: hut Date: Thu, 26 Aug 2010 17:25:50 +0200 Subject: Fixed history --- ranger/container/history.py | 21 ++++++++++++++++++++- ranger/core/actions.py | 2 +- ranger/defaults/commands.py | 2 +- ranger/defaults/options.py | 2 +- ranger/gui/widgets/console.py | 13 ++++++++----- ranger/help/console.py | 24 ++++++------------------ test/tc_history.py | 19 +++++++++++++++++++ 7 files changed, 56 insertions(+), 27 deletions(-) (limited to 'test') diff --git a/ranger/container/history.py b/ranger/container/history.py index ba13775d..5d4da07a 100644 --- a/ranger/container/history.py +++ b/ranger/container/history.py @@ -63,7 +63,7 @@ class History(object): self.history_forward.appendleft( self.history.pop() ) return self.current() - def move(self, n): + def move(self, n, pattern=None): if n > 0: return self.forward() if n < 0: @@ -84,3 +84,22 @@ class History(object): if self.history_forward: self.history.extend(self.history_forward) self.history_forward.clear() + + def unique(self): + found = [] + i = len(self.history) + while i: + i -= 1 + item = self.history[i] + if item in found: + del self.history[i] + else: + found.append(item) + i = len(self.history_forward) + while i: + i -= 1 + item = self.history_forward[i] + if item in found: + del self.history_forward[i] + else: + found.append(item) diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 6b8fc13c..d12d9c6c 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -86,7 +86,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.ui.console.line = string self.ui.console.execute() - def substitute_metachars(self, string): + def substitute_macros(self, string): return _MacroTemplate(string).safe_substitute(self._get_macros()) def _get_macros(self): diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py index 8005100c..b6e77697 100644 --- a/ranger/defaults/commands.py +++ b/ranger/defaults/commands.py @@ -117,7 +117,7 @@ class shell(Command): if not command and 'p' in flags: command = 'cat %f' if command: if '%' in command: - command = self.fm.substitute_metachars(command) + command = self.fm.substitute_macros(command) self.fm.execute_command(command, flags=flags) def tab(self): diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index 23752d97..d3c420af 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -85,7 +85,7 @@ tilde_in_titlebar = True # How many directory-changes or console-commands should be kept in history? max_history_size = 20 -max_console_history_size = 20 +max_console_history_size = 50 # Try to keep so much space between the top/bottom border when scrolling: scroll_offset = 8 diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 636073fc..2efb059d 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -34,6 +34,7 @@ import ranger class Console(Widget): visible = False last_cursor_mode = None + history_search_pattern = None prompt = ':' copy = '' tab_deque = None @@ -51,16 +52,16 @@ class Console(Widget): if not ranger.arg.clean: self.historypath = relpath_conf('history') try: - f = open(path, 'r') + f = open(self.historypath, 'r') except: pass else: for line in f: - hist.add(line[:-1]) + self.history.add(line[:-1]) f.close() def destroy(self): - # save histories from files + # save history to files if ranger.arg.clean or not self.settings.save_console_history: return if self.historypath: @@ -69,7 +70,7 @@ class Console(Widget): except: pass else: - for entry in self.histories[i]: + for entry in self.history: f.write(entry + '\n') f.close() @@ -173,6 +174,7 @@ class Console(Widget): self.pos += len(key) self.on_line_change() + self.history_search_pattern = self.line def history_move(self, n): try: @@ -182,7 +184,7 @@ class Console(Widget): else: if self.line != current and self.line != self.history.top(): self.history.modify(self.line) - self.history.move(n) + self.history.move(n, self.history_search_pattern) current = self.history.current() if self.line != current: self.line = self.history.current() @@ -191,6 +193,7 @@ class Console(Widget): def add_to_history(self): self.history.fast_forward() self.history.modify(self.line) + self.history.unique() def move(self, **keywords): direction = Direction(keywords) diff --git a/ranger/help/console.py b/ranger/help/console.py index bc69f7fb..f03491db 100644 --- a/ranger/help/console.py +++ b/ranger/help/console.py @@ -139,29 +139,17 @@ done typing and executes the command right away. The key "f" opens the console with ":find " 3.3.2. "shell" -The shell command accepts flags |25?| as the first argument if it starts -with a "-". Example: ":shell -p cat somefile.txt" will use the "p"-flag, -which pipes the output to the pager. -There are some keys which open the console with the shell command: +The shell command accepts flags |25?| as the first argument. This example +will use the "p"-flag, which pipes the output to the pager: + :shell -p cat somefile.txt + +There are some shortcuts which open the console with the shell command: "!" opens ":shell " "@" opens ":shell %s" "#" opens ":shell -p " 3.3.3. "open_with" -The open_with command opens the current file with the specified program, -mode |24?| and flags |25?|. -The programs and the meaning of modes can be defined in the apps.py, -giving you a high level interface for running files. -Pressing "r" will open the console with ":open_with " - -Examples: - -:open_with mplayer D open the selection in mplayer, but not detached -:open_with 1 open it with the default handler in mode 1 -:open_with d open it detached with the default handler -:open_with p open it as usual, but pipe the output to "less" -:open_with totem 1 Ds open in totem in mode 1, will not detach the - process (flag D) but discard the output (flag s) +The open_with command is explained in detail in chapter 2.2. |22?| ============================================================================== """ diff --git a/test/tc_history.py b/test/tc_history.py index 33784e14..b1161f2a 100644 --- a/test/tc_history.py +++ b/test/tc_history.py @@ -54,4 +54,23 @@ class Test(TestCase): self.assertEqual(4, hist.bottom()) self.assertEqual([4,5,6], list(hist)) + hist.back() + hist.fast_forward() + self.assertEqual([4,5,6], list(hist)) + hist.back() + hist.back() + hist.fast_forward() + self.assertEqual([4,5,6], list(hist)) + hist.back() + hist.back() + hist.back() + hist.fast_forward() + self.assertEqual([4,5,6], list(hist)) + hist.back() + hist.back() + hist.back() + hist.back() + hist.fast_forward() + self.assertEqual([4,5,6], list(hist)) + if __name__ == '__main__': main() -- cgit 1.4.1-2-gfad0 From a02baa3311a469a021ed0f801d3a743ecb1ace41 Mon Sep 17 00:00:00 2001 From: hut Date: Sat, 28 Aug 2010 08:01:22 +0200 Subject: Changed implementation of container.history --- ranger/container/history.py | 131 ++++++++++++++++++++++++-------------------- test/tc_history.py | 16 +++--- 2 files changed, 80 insertions(+), 67 deletions(-) (limited to 'test') diff --git a/ranger/container/history.py b/ranger/container/history.py index 5d4da07a..9a3d6c4a 100644 --- a/ranger/container/history.py +++ b/ranger/container/history.py @@ -13,93 +13,106 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from collections import deque - class HistoryEmptyException(Exception): pass class History(object): - def __init__(self, maxlen = None): - self.history = deque(maxlen = maxlen) - self.history_forward = deque(maxlen = maxlen) + def __init__(self, maxlen=None): + self._history = [] + self._index = 0 + self.maxlen = maxlen def add(self, item): - if len(self.history) == 0 or self.history[-1] != item: - self.history.append(item) - self.history_forward.clear() - - def modify(self, item): + # Remove Duplicates + try: + self._history.remove(item) + except: + pass + # Remove first if list is too long + if len(self._history) > self.maxlen - 1: + del self._history[0] + # Append the item and fast forward + self._history.append(item) + self._index = len(self._history) - 1 + + def modify(self, item, unique=False): + if self._history and unique: + try: + self._history.remove(item) + self._index -= 1 + except: + pass try: - self.history[-1] = item + self._history[self._index] = item except IndexError: - raise HistoryEmptyException + self.add(item) def __len__(self): - return len(self.history) + return len(self._history) def current(self): - try: - return self.history[-1] - except IndexError: - raise HistoryEmptyException() + if self._history: + return self._history[self._index] + else: + raise HistoryEmptyException def top(self): try: - return self.history_forward[-1] + return self._history[-1] except IndexError: - try: - return self.history[-1] - except IndexError: - raise HistoryEmptyException() + raise HistoryEmptyException() def bottom(self): try: - return self.history[0] + return self._history[0] except IndexError: raise HistoryEmptyException() def back(self): - if len(self.history) > 1: - self.history_forward.appendleft( self.history.pop() ) - return self.current() - - def move(self, n, pattern=None): - if n > 0: - return self.forward() - if n < 0: - return self.back() + self._index -= 1 + if self._index < 0: + self._index = 0 + + def move(self, n): + self._index += n + if self._index > len(self._history) - 1: + self._index = len(self._history) - 1 + if self._index < 0: + self._index = 0 + + def search(self, string, n): + if n != 0 and string: + step = n > 0 and 1 or -1 + i = self._index + steps_left = steps_left_at_start = int(abs(n)) + while steps_left: + i += step + if i >= len(self._history) or i < 0: + break + if self._history[i].startswith(string): + steps_left -= 1 + if steps_left != steps_left_at_start: + self._index = i def __iter__(self): - return self.history.__iter__() + return self._history.__iter__() def next(self): - return self.history.next() + return self._history.next() def forward(self): - if len(self.history_forward) > 0: - self.history.append( self.history_forward.popleft() ) - return self.current() + if self._history: + self._index += 1 + if self._index > len(self._history) - 1: + self._index = len(self._history) - 1 + else: + self._index = 0 def fast_forward(self): - if self.history_forward: - self.history.extend(self.history_forward) - self.history_forward.clear() - - def unique(self): - found = [] - i = len(self.history) - while i: - i -= 1 - item = self.history[i] - if item in found: - del self.history[i] - else: - found.append(item) - i = len(self.history_forward) - while i: - i -= 1 - item = self.history_forward[i] - if item in found: - del self.history_forward[i] - else: - found.append(item) + if self._history: + self._index = len(self._history) - 1 + else: + self._index = 0 + + def _left(self): # used for unit test + return self._history[0:self._index+1] diff --git a/test/tc_history.py b/test/tc_history.py index b1161f2a..301ebedd 100644 --- a/test/tc_history.py +++ b/test/tc_history.py @@ -27,13 +27,13 @@ class Test(TestCase): hist.back() self.assertEqual(4, hist.current()) - self.assertEqual([3,4], list(hist)) + self.assertEqual([3,4], list(hist._left())) self.assertEqual(5, hist.top()) hist.back() self.assertEqual(3, hist.current()) - self.assertEqual([3], list(hist)) + self.assertEqual([3], list(hist._left())) # no change if current == bottom self.assertEqual(hist.current(), hist.bottom()) @@ -46,31 +46,31 @@ class Test(TestCase): hist.forward() hist.forward() self.assertEqual(5, hist.current()) - self.assertEqual([3,4,5], list(hist)) + self.assertEqual([3,4,5], list(hist._left())) self.assertEqual(3, hist.bottom()) hist.add(6) self.assertEqual(4, hist.bottom()) - self.assertEqual([4,5,6], list(hist)) + self.assertEqual([4,5,6], list(hist._left())) hist.back() hist.fast_forward() - self.assertEqual([4,5,6], list(hist)) + self.assertEqual([4,5,6], list(hist._left())) hist.back() hist.back() hist.fast_forward() - self.assertEqual([4,5,6], list(hist)) + self.assertEqual([4,5,6], list(hist._left())) hist.back() hist.back() hist.back() hist.fast_forward() - self.assertEqual([4,5,6], list(hist)) + self.assertEqual([4,5,6], list(hist._left())) hist.back() hist.back() hist.back() hist.back() hist.fast_forward() - self.assertEqual([4,5,6], list(hist)) + self.assertEqual([4,5,6], list(hist._left())) if __name__ == '__main__': main() -- cgit 1.4.1-2-gfad0 From ec823be00aa7175ebcb836a240a20f6389159c2e Mon Sep 17 00:00:00 2001 From: hut Date: Sun, 29 Aug 2010 19:16:57 +0200 Subject: Removed symlink in test/ --- test/all_benchmarks.py | 8 ++++++-- test/all_tests.py | 6 +++++- test/bm_human_readable.py | 6 ++++++ test/bm_loader.py | 6 ++++++ test/ranger | 1 - test/tc_bookmarks.py | 6 ++++++ test/tc_colorscheme.py | 6 ++++++ test/tc_direction.py | 6 ++++++ test/tc_directory.py | 5 +++++ test/tc_displayable.py | 6 ++++++ test/tc_ext.py | 6 ++++++ test/tc_history.py | 6 ++++++ test/tc_human_readable.py | 6 ++++++ test/tc_keyapi.py | 6 ++++++ test/tc_loader.py | 6 ++++++ test/tc_newkeys.py | 10 +++++++--- test/tc_signal.py | 6 ++++++ test/tc_ui.py | 6 ++++++ test/tc_utfwidth.py | 8 +++++++- 19 files changed, 108 insertions(+), 8 deletions(-) delete mode 120000 test/ranger (limited to 'test') diff --git a/test/all_benchmarks.py b/test/all_benchmarks.py index 20f11ad8..a3612701 100755 --- a/test/all_benchmarks.py +++ b/test/all_benchmarks.py @@ -19,9 +19,13 @@ Run all the benchmarks inside this directory. Usage: ./all_benchmarks.py [count] [regexp-filters...] """ -import os -import re +import os.path import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + +import re import time if __name__ == '__main__': diff --git a/test/all_tests.py b/test/all_tests.py index 7cfc855f..0c184df5 100755 --- a/test/all_tests.py +++ b/test/all_tests.py @@ -19,8 +19,12 @@ Run all the tests inside this directory as a test suite. Usage: ./all_tests.py [verbosity] """ -import os +import os.path import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest if __name__ == '__main__': diff --git a/test/bm_human_readable.py b/test/bm_human_readable.py index 83f2a057..ef400774 100644 --- a/test/bm_human_readable.py +++ b/test/bm_human_readable.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from ranger.ext.human_readable import * # The version before 2010/06/24: diff --git a/test/bm_loader.py b/test/bm_loader.py index 968640a5..552954a7 100644 --- a/test/bm_loader.py +++ b/test/bm_loader.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from ranger.core.loader import Loader from ranger.fsobject import Directory, File from ranger.ext.openstruct import OpenStruct diff --git a/test/ranger b/test/ranger deleted file mode 120000 index 5459458c..00000000 --- a/test/ranger +++ /dev/null @@ -1 +0,0 @@ -../ranger \ No newline at end of file diff --git a/test/tc_bookmarks.py b/test/tc_bookmarks.py index 9b41f1c6..59435f06 100644 --- a/test/tc_bookmarks.py +++ b/test/tc_bookmarks.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from os.path import realpath, join, dirname import unittest import os diff --git a/test/tc_colorscheme.py b/test/tc_colorscheme.py index 8d6adee6..eefb1e4f 100644 --- a/test/tc_colorscheme.py +++ b/test/tc_colorscheme.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from unittest import TestCase, main import random import ranger.colorschemes diff --git a/test/tc_direction.py b/test/tc_direction.py index 5c1776cf..16c26dab 100644 --- a/test/tc_direction.py +++ b/test/tc_direction.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest from ranger.ext.direction import Direction from ranger.ext.openstruct import OpenStruct diff --git a/test/tc_directory.py b/test/tc_directory.py index a702c4db..754253b3 100644 --- a/test/tc_directory.py +++ b/test/tc_directory.py @@ -13,7 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from os.path import realpath, join, dirname from ranger import fsobject diff --git a/test/tc_displayable.py b/test/tc_displayable.py index 1c66a40e..72e0507d 100644 --- a/test/tc_displayable.py +++ b/test/tc_displayable.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest import curses from random import randint diff --git a/test/tc_ext.py b/test/tc_ext.py index 919f35a2..495591a1 100644 --- a/test/tc_ext.py +++ b/test/tc_ext.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest from collections import deque diff --git a/test/tc_history.py b/test/tc_history.py index 301ebedd..02a8bb9f 100644 --- a/test/tc_history.py +++ b/test/tc_history.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from ranger.container import History from unittest import TestCase, main import unittest diff --git a/test/tc_human_readable.py b/test/tc_human_readable.py index b931ba21..493e6d3a 100644 --- a/test/tc_human_readable.py +++ b/test/tc_human_readable.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest from ranger.ext.human_readable import human_readable as hr diff --git a/test/tc_keyapi.py b/test/tc_keyapi.py index 48282a7d..79d89fa5 100644 --- a/test/tc_keyapi.py +++ b/test/tc_keyapi.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from unittest import TestCase, main class Test(TestCase): diff --git a/test/tc_loader.py b/test/tc_loader.py index 9f7f7322..5a2e5a68 100644 --- a/test/tc_loader.py +++ b/test/tc_loader.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest import os from os.path import realpath, join, dirname diff --git a/test/tc_newkeys.py b/test/tc_newkeys.py index fd856f17..c9597201 100644 --- a/test/tc_newkeys.py +++ b/test/tc_newkeys.py @@ -12,7 +12,13 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . + +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] +sys.path[1:1] = ['..'] from unittest import TestCase, main @@ -22,8 +28,6 @@ from ranger.container.keymap import * from ranger.container.keybuffer import KeyBuffer from ranger.ext.keybinding_parser import parse_keybinding -import sys - def simulate_press(self, string): for char in parse_keybinding(string): self.add(char) diff --git a/test/tc_signal.py b/test/tc_signal.py index f31681f4..3b1bac40 100644 --- a/test/tc_signal.py +++ b/test/tc_signal.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest import gc from ranger.ext.signal_dispatcher import * diff --git a/test/tc_ui.py b/test/tc_ui.py index dc8d669d..fa2bdcac 100644 --- a/test/tc_ui.py +++ b/test/tc_ui.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest import curses diff --git a/test/tc_utfwidth.py b/test/tc_utfwidth.py index 2aa5fa6d..0288c17b 100644 --- a/test/tc_utfwidth.py +++ b/test/tc_utfwidth.py @@ -12,7 +12,13 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . + +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] +sys.path[1:1] = ['..'] from unittest import TestCase, main from ranger.ext.utfwidth import * -- cgit 1.4.1-2-gfad0 From a1032643b32f446bccce1253aab7c470c1484d31 Mon Sep 17 00:00:00 2001 From: hut Date: Sun, 29 Aug 2010 19:58:28 +0200 Subject: defaults.keys: key pL to create relative symlinks --- ranger/core/actions.py | 8 +++++-- ranger/defaults/keys.py | 5 +++-- ranger/ext/relative_symlink.py | 39 +++++++++++++++++++++++++++++++++++ ranger/help/fileop.py | 3 ++- test/tc_relative_symlink.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 ranger/ext/relative_symlink.py create mode 100644 test/tc_relative_symlink.py (limited to 'test') diff --git a/ranger/core/actions.py b/ranger/core/actions.py index a5ee0d4d..a93344a4 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -23,6 +23,7 @@ from inspect import cleandoc import ranger from ranger.ext.direction import Direction +from ranger.ext.relative_symlink import relative_symlink from ranger.ext.shell_escape import shell_quote from ranger import fsobject from ranger.shared import FileManagerAware, EnvironmentAware, SettingsAware @@ -660,11 +661,14 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.env.cut = True self.ui.browser.main_column.request_redraw() - def paste_symlink(self): + def paste_symlink(self, relative=False): copied_files = self.env.copy for f in copied_files: try: - symlink(f.path, join(getcwd(), f.basename)) + if relative: + relative_symlink(f.path, join(getcwd(), f.basename)) + else: + symlink(f.path, join(getcwd(), f.basename)) except Exception as x: self.notify(x) diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py index 7d8ccc4a..9f0c78cb 100644 --- a/ranger/defaults/keys.py +++ b/ranger/defaults/keys.py @@ -175,9 +175,10 @@ map('da', fm.cut(mode='add')) map('dr', fm.cut(mode='remove')) map('pp', fm.paste()) map('po', fm.paste(overwrite=True)) -map('pl', fm.paste_symlink()) +map('pl', fm.paste_symlink(relative=False)) +map('pL', fm.paste_symlink(relative=True)) map('p', fm.hint('press *p* to confirm pasting' \ - ', *o* to overwrite or *l* to create symlinks')) + ', *o*verwrite, create sym*l*inks, relative sym*L*inks')) map('u', fm.hint("un*y*ank, unbook*m*ark, unselect:*v*")) map('ud', 'uy', fm.uncut()) diff --git a/ranger/ext/relative_symlink.py b/ranger/ext/relative_symlink.py new file mode 100644 index 00000000..bba00e39 --- /dev/null +++ b/ranger/ext/relative_symlink.py @@ -0,0 +1,39 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from os import symlink, sep +from os.path import dirname, join + +def relative_symlink(src, dst): + common_base = get_common_base(src, dst) + symlink(get_relative_source_file(src, dst, common_base), dst) + +def get_relative_source_file(src, dst, common_base=None): + if common_base is None: + common_base = get_common_base(src, dst) + return '../' * dst.count('/', len(common_base)) + src[len(common_base):] + +def get_common_base(src, dst): + if not src or not dst: + return '/' + i = 0 + while True: + new_i = src.find(sep, i + 1) + if new_i == -1: + break + if not dst.startswith(src[:new_i + 1]): + break + i = new_i + return src[:i + 1] diff --git a/ranger/help/fileop.py b/ranger/help/fileop.py index f8401800..ac23c6d4 100644 --- a/ranger/help/fileop.py +++ b/ranger/help/fileop.py @@ -31,7 +31,7 @@ harm your files: :chmod Change the rights of the selection :delete DELETES ALL FILES IN THE SELECTION :rename Change the name of the current file -pp, pl, po Pastes the copied files in different ways +pp, pl, pL, po Pastes the copied files in different ways Think twice before using these commands or key combinations. @@ -67,6 +67,7 @@ The "highlighted file", or the "current file", is the one below the cursor. Instead, a "_" character will be appended to the new filename. po paste the copied/cut files. Existing files are overwritten. pl create symbolic links to the copied/cut files. + pL create relative symbolic links to the copied/cut files. The difference between copying and cutting should be intuitive: diff --git a/test/tc_relative_symlink.py b/test/tc_relative_symlink.py new file mode 100644 index 00000000..a202513d --- /dev/null +++ b/test/tc_relative_symlink.py @@ -0,0 +1,47 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + +import unittest +from ranger.ext.relative_symlink import * +rel = get_relative_source_file + +class Test(unittest.TestCase): + def test_foo(self): + self.assertEqual('../foo', rel('/foo', '/x/bar')) + self.assertEqual('../../foo', rel('/foo', '/x/y/bar')) + self.assertEqual('../../a/b/foo', rel('/a/b/foo', '/x/y/bar')) + self.assertEqual('../../x/b/foo', rel('/x/b/foo', '/x/y/bar', + common_base='/')) + self.assertEqual('../b/foo', rel('/x/b/foo', '/x/y/bar')) + self.assertEqual('../b/foo', rel('/x/b/foo', '/x/y/bar')) + + def test_get_common_base(self): + self.assertEqual('/', get_common_base('', '')) + self.assertEqual('/', get_common_base('', '/')) + self.assertEqual('/', get_common_base('/', '')) + self.assertEqual('/', get_common_base('/', '/')) + self.assertEqual('/', get_common_base('/bla/bar/x', '/foo/bar/a')) + self.assertEqual('/foo/bar/', get_common_base('/foo/bar/x', '/foo/bar/a')) + self.assertEqual('/foo/', get_common_base('/foo/bar/x', '/foo/baz/a')) + self.assertEqual('/foo/', get_common_base('/foo/bar/x', '/foo/baz/a')) + self.assertEqual('/', get_common_base('//foo/bar/x', '/foo/baz/a')) + +if __name__ == '__main__': unittest.main() -- cgit 1.4.1-2-gfad0 From c9dd8d5e0641fecc73e6cb8aa6fe2ffaa6dec4a6 Mon Sep 17 00:00:00 2001 From: hut Date: Sun, 29 Aug 2010 19:59:16 +0200 Subject: removed test/__init__.py (not needed) --- test/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/__init__.py (limited to 'test') diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29b..00000000 -- cgit 1.4.1-2-gfad0