diff --git a/.gitignore b/.gitignore index 1ff8381a..875204c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .idea -MANIFEST \ No newline at end of file +MANIFEST +*.pyc +.tox +build/ diff --git a/AUTHORS b/AUTHORS index 0e169a1f..8a1b7410 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,4 +12,15 @@ Patches and Suggestions - Jeff Forcier - Morgan Goose -- Travis Swicegood \ No newline at end of file +- Travis Swicegood +- Will Thames +- Greg Haskins +- Miguel Araujo +- takluyver +- kracekumar +- Alejandro Gómez +- Jason Piper +- Gianluca Brindisi +- Don Spaulding +- Justin Barber +- Dmitry Medvinsky diff --git a/HACKING b/HACKING index 018f9b7a..46b9651f 100644 --- a/HACKING +++ b/HACKING @@ -11,4 +11,4 @@ All functionality should be available in pure Python. Optional C (via Cython) implementations may be written for performance reasons, but should never replace the Python implementation. -Lastly, don't take yourself too seriously :) \ No newline at end of file +Lastly, don't take yourself too seriously :) diff --git a/HISTORY.rst b/HISTORY.rst index 4af4b7ff..d13983ec 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,35 @@ History ------- +0.3.0 ++++++ + +* Python 3 support! + +0.2.4 ++++++ + +* New eng module +* Win32 Bugfix + + +0.2.3 ++++++ + +* Only init colors if they are used (iPython compatability) +* New progress module +* Various bugfixes + + +0.2.2 ++++++ + +* Auto Color Disabling +* Progress Namespace Change +* New Progress Bars +* textui.puts newline fix + + 0.2.1 (2011-03-24) ++++++++++++++++++ diff --git a/README.rst b/README.rst index 05fd4dbd..2badfb9f 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Clint: Python Command-line Application Tools **Clint** is a module filled with a set of awesome tools for developing commandline applications. -.. image:: https://github.com/kennethreitz/clint/raw/master/misc/clint.jpeg +.. image:: https://raw.github.com/kennethreitz/clint/develop/misc/clint.jpeg **C** ommand **L** ine @@ -15,30 +15,40 @@ commandline applications. Clint is awesome. Crazy awesome. It supports colors, but detects if the session is a TTY, so doesn't render the colors if you're piping stuff around. Automagically. -Awesome nestable indentation context manager. Example: (``with indent(4): puts('indented text')``). It supports custom email-style quotes. Of course, it supports color too, if and when needed. +Awesome nest-able indentation context manager. Example: (``with indent(4): puts('indented text')``). It supports custom email-style quotes. Of course, it supports color too, if and when needed. It has an awesome Column printer with optional auto-expanding columns. It detects how wide your current console is and adjusts accordingly. It wraps your words properly to fit the column size. With or without colors mixed in. All with a single function call. The world's easiest to use implicit argument system w/ chaining methods for filtering. Seriously. -Run the various executables in ``./examples`` to get a good feel for what Clint offers. +Run the various executables in examples_ to get a good feel for what Clint offers. +.. _examples: https://github.com/kennethreitz/clint/tree/develop/examples You'll never want to not use it. - -Features: ---------- +Current Features: +----------------- - Little Documentation (bear with me for now) - CLI Colors and Indents +- Extremely Simple + Powerful Column Printer - Iterator-based Progress Bar - Implicit Argument Handling -- Simple Support for Unix Pipes +- Simple Support for Incoming Unix Pipes - Application Directory management + +Future Features: +---------------- +- Documentation! +- Simple choice system ``Are you sure? [Yn]`` +- Default query system ``Installation Path [/usr/local/bin/]`` +- Suggestions welcome. + + Example ------- @@ -60,7 +70,7 @@ I want to quote my console text (like email). :: >>> puts('pretty cool, eh?') not indented text - > indented text + > quoted text > pretty cool, eh? I want to color my console text. :: @@ -150,4 +160,4 @@ Roadmap .. _`the repository`: http://github.com/kennethreitz/clint -.. _AUTHORS: http://github.com/kennethreitz/clint/blob/master/AUTHORS +.. _AUTHORS: http://github.com/kennethreitz/clint/blob/develop/AUTHORS diff --git a/clint/__init__.py b/clint/__init__.py index 29ec2d21..7e52439c 100644 --- a/clint/__init__.py +++ b/clint/__init__.py @@ -11,7 +11,14 @@ from __future__ import absolute_import -from . import arguments +try: + from collections import OrderedDict +except ImportError: + from .packages.ordereddict import OrderedDict + import collections + collections.OrderedDict = OrderedDict + +from arguments import * from . import textui from . import utils from .pipes import piped_in @@ -19,12 +26,9 @@ __title__ = 'clint' -__version__ = '0.2.1' -__build__ = 0x000201 +__version__ = '0.3.0' +__build__ = 0x000300 __author__ = 'Kenneth Reitz' __license__ = 'ISC' -__copyright__ = 'Copyright 2011 Kenneth Reitz' +__copyright__ = 'Copyright 2012 Kenneth Reitz' __docformat__ = 'restructuredtext' - - -args = arguments.Args() diff --git a/clint/arguments.py b/clint/arguments.py index 579a2e0a..d13622a5 100644 --- a/clint/arguments.py +++ b/clint/arguments.py @@ -13,33 +13,17 @@ import os from sys import argv -from glob import glob - -from .packages.ordereddict import OrderedDict -from .utils import is_collection +try: + from collections import OrderedDict +except ImportError: + from .packages.ordereddict import OrderedDict +from .utils import expand_path, is_collection __all__ = ('Args', ) - -def _expand_path(path): - """Expands directories and globs in given path.""" - - paths = [] - - if os.path.isdir(path): - - for (dir, dirs, files) in os.walk(path): - for file in files: - paths.append(os.path.join(dir, file)) - else: - paths.extend(glob(path)) - - return paths - - class Args(object): """CLI Argument management.""" @@ -110,20 +94,20 @@ def pop(self, x): def any_contain(self, x): """Tests if given string is contained in any stored argument.""" - + return bool(self.first_with(x)) def contains(self, x): - """Tests if given object is in arguments list. + """Tests if given object is in arguments list. Accepts strings and lists of strings.""" - + return self.__contains__(x) def first(self, x): """Returns first found index of given value (or list of values)""" - + def _find( x): try: return self.all.index(str(x)) @@ -212,7 +196,7 @@ def contains_at(self, x, index): return False else: return (x in self.all[index]) - + except IndexError: return False @@ -221,7 +205,7 @@ def has(self, x): """Returns true if argument exists at given index. Accepts: integer. """ - + try: self.all[x] return True @@ -231,15 +215,15 @@ def has(self, x): def value_after(self, x): """Returns value of argument after given found argument (or list thereof).""" - + try: try: i = self.all.index(x) except ValueError: return None - + return self.all[i + 1] - + except IndexError: return None @@ -257,7 +241,7 @@ def grouped(self): for arg in self.all: if arg.startswith('-'): _current_group = arg - collection[arg] = Args(no_argv=True) + collection.setdefault(arg, Args(no_argv=True)) else: if _current_group: collection[_current_group]._args.append(arg) @@ -266,21 +250,21 @@ def grouped(self): return collection - + @property def last(self): """Returns last argument.""" - + try: return self.all[-1] except IndexError: return None - + @property def all(self): """Returns all arguments.""" - + return self._args @@ -288,7 +272,7 @@ def all_with(self, x): """Returns all arguments containing given string (or list thereof)""" _args = [] - + for arg in self.all: if is_collection(x): for _x in x: @@ -327,7 +311,7 @@ def flags(self): return self.start_with('-') - @property + @property def not_flags(self): """Returns Arg object excluding flagged arguments.""" @@ -341,7 +325,7 @@ def files(self, absolute=False): _paths = [] for arg in self.all: - for path in _expand_path(arg): + for path in expand_path(arg): if os.path.exists(path): if absolute: _paths.append(os.path.abspath(path)) @@ -358,7 +342,7 @@ def not_files(self): _args = [] for arg in self.all: - if not len(_expand_path(arg)): + if not len(expand_path(arg)): if not os.path.exists(arg): _args.append(arg) diff --git a/clint/eng.py b/clint/eng.py new file mode 100644 index 00000000..ab49ee91 --- /dev/null +++ b/clint/eng.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +""" +clint.eng +~~~~~~~~~ + +This module provides English language string helpers. + +""" +from __future__ import print_function + +MORON_MODE = False +COMMA = ',' +CONJUNCTION = 'and' +SPACE = ' ' + +try: + unicode +except NameError: + unicode = str + + +def join(l, conj=CONJUNCTION, im_a_moron=MORON_MODE, separator=COMMA): + """Joins lists of words. Oxford comma and all.""" + + collector = [] + left = len(l) + separator = separator + SPACE + conj = conj + SPACE + + for _l in l[:]: + + left += -1 + + collector.append(_l) + if left == 1: + if len(l) == 2 or im_a_moron: + collector.append(SPACE) + else: + collector.append(separator) + + collector.append(conj) + + elif left is not 0: + collector.append(separator) + + return unicode(str().join(collector)) + +if __name__ == '__main__': + print(join(['blue', 'red', 'yellow'], conj='or', im_a_moron=True)) + print(join(['blue', 'red', 'yellow'], conj='or')) + print(join(['blue', 'red'], conj='or')) + print(join(['blue', 'red'], conj='and')) + print(join(['blue'], conj='and')) + print(join(['blue', 'red', 'yellow', 'green', 'ello'], conj='and')) diff --git a/clint/packages/colorama/__init__.py b/clint/packages/colorama/__init__.py index 331174e5..147a3e03 100644 --- a/clint/packages/colorama/__init__.py +++ b/clint/packages/colorama/__init__.py @@ -1,6 +1,6 @@ -from .initialise import init +from .initialise import init, deinit, reinit from .ansi import Fore, Back, Style from .ansitowin32 import AnsiToWin32 -VERSION = '0.1.18' +VERSION = '0.2.3' diff --git a/clint/packages/colorama/ansitowin32.py b/clint/packages/colorama/ansitowin32.py index 363061d3..489a9175 100644 --- a/clint/packages/colorama/ansitowin32.py +++ b/clint/packages/colorama/ansitowin32.py @@ -118,12 +118,12 @@ def write(self, text): self.wrapped.flush() if self.autoreset: self.reset_all() - + def reset_all(self): if self.convert: self.call_win32('m', (0,)) - else: + elif is_a_tty(self.wrapped): self.wrapped.write(Style.RESET_ALL) @@ -173,4 +173,10 @@ def call_win32(self, command, params): args = func_args[1:] kwargs = dict(on_stderr=self.on_stderr) func(*args, **kwargs) + elif command in ('H', 'f'): # set cursor position + func = winterm.set_cursor_position + func(params, on_stderr=self.on_stderr) + elif command in ('J'): + func = winterm.erase_data + func(params, on_stderr=self.on_stderr) diff --git a/clint/packages/colorama/initialise.py b/clint/packages/colorama/initialise.py index 4df5c3e3..51aaa34b 100644 --- a/clint/packages/colorama/initialise.py +++ b/clint/packages/colorama/initialise.py @@ -7,6 +7,9 @@ orig_stdout = sys.stdout orig_stderr = sys.stderr +wrapped_stdout = sys.stdout +wrapped_stderr = sys.stderr + atexit_done = False @@ -16,11 +19,14 @@ def reset_all(): def init(autoreset=False, convert=None, strip=None, wrap=True): - if wrap==False and (autoreset==True or convert==True or strip==True): + if not wrap and any([autoreset, convert, strip]): raise ValueError('wrap=False conflicts with any other arg=True') - sys.stdout = wrap_stream(orig_stdout, convert, strip, autoreset, wrap) - sys.stderr = wrap_stream(orig_stderr, convert, strip, autoreset, wrap) + global wrapped_stdout, wrapped_stderr + sys.stdout = wrapped_stdout = \ + wrap_stream(orig_stdout, convert, strip, autoreset, wrap) + sys.stderr = wrapped_stderr = \ + wrap_stream(orig_stderr, convert, strip, autoreset, wrap) global atexit_done if not atexit_done: @@ -28,6 +34,16 @@ def init(autoreset=False, convert=None, strip=None, wrap=True): atexit_done = True +def deinit(): + sys.stdout = orig_stdout + sys.stderr = orig_stderr + + +def reinit(): + sys.stdout = wrapped_stdout + sys.stderr = wrapped_stdout + + def wrap_stream(stream, convert, strip, autoreset, wrap): if wrap: wrapper = AnsiToWin32(stream, @@ -36,3 +52,4 @@ def wrap_stream(stream, convert, strip, autoreset, wrap): stream = wrapper.stream return stream + diff --git a/clint/packages/colorama/win32.py b/clint/packages/colorama/win32.py index 2a6fc949..ed4d613e 100644 --- a/clint/packages/colorama/win32.py +++ b/clint/packages/colorama/win32.py @@ -48,8 +48,16 @@ class CONSOLE_SCREEN_BUFFER_INFO(Structure): ("srWindow", SMALL_RECT), ("dwMaximumWindowSize", COORD), ] + def __str__(self): + return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( + self.dwSize.Y, self.dwSize.X + , self.dwCursorPosition.Y, self.dwCursorPosition.X + , self.wAttributes + , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right + , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X + ) - def GetConsoleScreenBufferInfo(stream_id): + def GetConsoleScreenBufferInfo(stream_id=STDOUT): handle = handles[stream_id] csbi = CONSOLE_SCREEN_BUFFER_INFO() success = windll.kernel32.GetConsoleScreenBufferInfo( @@ -62,34 +70,54 @@ def GetConsoleScreenBufferInfo(stream_id): def SetConsoleTextAttribute(stream_id, attrs): handle = handles[stream_id] - success = windll.kernel32.SetConsoleTextAttribute(handle, attrs) - assert success + return windll.kernel32.SetConsoleTextAttribute(handle, attrs) def SetConsoleCursorPosition(stream_id, position): - handle = handles[stream_id] position = COORD(*position) - success = windll.kernel32.SetConsoleCursorPosition(handle, position) - assert success + # If the position is out of range, do nothing. + if position.Y <= 0 or position.X <= 0: + return + # Adjust for Windows' SetConsoleCursorPosition: + # 1. being 0-based, while ANSI is 1-based. + # 2. expecting (x,y), while ANSI uses (y,x). + adjusted_position = COORD(position.Y - 1, position.X - 1) + # Adjust for viewport's scroll position + sr = GetConsoleScreenBufferInfo(STDOUT).srWindow + adjusted_position.Y += sr.Top + adjusted_position.X += sr.Left + # Resume normal processing + handle = handles[stream_id] + success = windll.kernel32.SetConsoleCursorPosition(handle, adjusted_position) + return success def FillConsoleOutputCharacter(stream_id, char, length, start): handle = handles[stream_id] char = TCHAR(char) length = DWORD(length) - start = COORD(*start) num_written = DWORD(0) - # AttributeError: function 'FillConsoleOutputCharacter' not found - # could it just be that my types are wrong? - success = windll.kernel32.FillConsoleOutputCharacter( + # Note that this is hard-coded for ANSI (vs wide) bytes. + success = windll.kernel32.FillConsoleOutputCharacterA( handle, char, length, start, byref(num_written)) - assert success return num_written.value + def FillConsoleOutputAttribute(stream_id, attr, length, start): + ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )''' + handle = handles[stream_id] + attribute = WORD(attr) + length = DWORD(length) + num_written = DWORD(0) + # Note that this is hard-coded for ANSI (vs wide) bytes. + success = windll.kernel32.FillConsoleOutputAttribute( + handle, attribute, length, start, byref(num_written)) + return success + if __name__=='__main__': x = GetConsoleScreenBufferInfo(STDOUT) - print(x.dwSize) - print(x.dwCursorPosition) - print(x.wAttributes) - print(x.srWindow) - print(x.dwMaximumWindowSize) + print(x) + print('dwSize(height,width) = (%d,%d)' % (x.dwSize.Y, x.dwSize.X)) + print('dwCursorPosition(y,x) = (%d,%d)' % (x.dwCursorPosition.Y, x.dwCursorPosition.X)) + print('wAttributes(color) = %d = 0x%02x' % (x.wAttributes, x.wAttributes)) + print('srWindow(Top,Left)-(Bottom,Right) = (%d,%d)-(%d,%d)' % (x.srWindow.Top, x.srWindow.Left, x.srWindow.Bottom, x.srWindow.Right)) + print('dwMaximumWindowSize(maxHeight,maxWidth) = (%d,%d)' % (x.dwMaximumWindowSize.Y, x.dwMaximumWindowSize.X)) diff --git a/clint/packages/colorama/winterm.py b/clint/packages/colorama/winterm.py index 4326c21b..95585f3f 100644 --- a/clint/packages/colorama/winterm.py +++ b/clint/packages/colorama/winterm.py @@ -22,8 +22,7 @@ class WinStyle(object): class WinTerm(object): def __init__(self): - self._default = \ - win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes + self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes self.set_attrs(self._default) self._default_fore = self._fore self._default_back = self._back @@ -67,3 +66,37 @@ def set_console(self, attrs=None, on_stderr=False): handle = win32.STDERR win32.SetConsoleTextAttribute(handle, attrs) + def set_cursor_position(self, position=None, on_stderr=False): + if position is None: + #I'm not currently tracking the position, so there is no default. + #position = self.get_position() + return + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + win32.SetConsoleCursorPosition(handle, position) + + def erase_data(self, mode=0, on_stderr=False): + # 0 (or None) should clear from the cursor to the end of the screen. + # 1 should clear from the cursor to the beginning of the screen. + # 2 should clear the entire screen. (And maybe move cursor to (1,1)?) + # + # At the moment, I only support mode 2. From looking at the API, it + # should be possible to calculate a different number of bytes to clear, + # and to do so relative to the cursor position. + if mode[0] not in (2,): + return + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + # here's where we'll home the cursor + coord_screen = win32.COORD(0,0) + csbi = win32.GetConsoleScreenBufferInfo(handle) + # get the number of character cells in the current buffer + dw_con_size = csbi.dwSize.X * csbi.dwSize.Y + # fill the entire screen with blanks + win32.FillConsoleOutputCharacter(handle, ord(' '), dw_con_size, coord_screen) + # now set the buffer's attributes accordingly + win32.FillConsoleOutputAttribute(handle, self.get_attrs(), dw_con_size, coord_screen ); + # put the cursor at (0, 0) + win32.SetConsoleCursorPosition(handle, (coord_screen.X, coord_screen.Y)) diff --git a/clint/resources.py b/clint/resources.py index 27ad10a2..b499a0fe 100644 --- a/clint/resources.py +++ b/clint/resources.py @@ -39,7 +39,7 @@ def __init__(self, path=None): def __repr__(self): return '' % (self.path) - + def __getattribute__(self, name): @@ -53,11 +53,11 @@ def _raise_if_none(self): """Raises if operations are carried out on an unconfigured AppDir.""" if not self.path: raise NotConfigured() - + def _create(self): """Creates current AppDir at AppDir.path.""" - + self._raise_if_none() if not self._exists: mkdir_p(self.path) @@ -66,7 +66,7 @@ def _create(self): def open(self, filename, mode='r'): """Returns file object from given filename.""" - + self._raise_if_none() fn = path_join(self.path, filename) @@ -115,17 +115,17 @@ def delete(self, filename=''): remove(fn) else: removedirs(fn) - except OSError, why: + except OSError as why: if why.errno == errno.ENOENT: pass else: raise why - + def read(self, filename, binary=False): """Returns contents of given file with AppDir. If file doesn't exist, returns None.""" - + self._raise_if_none() fn = path_join(self.path, filename) @@ -161,11 +161,11 @@ def sub(self, path): def init(vendor, name): global user, site, cache, log - + ad = AppDirs(name, vendor) user.path = ad.user_data_dir - + site.path = ad.site_data_dir cache.path = ad.user_cache_dir log.path = ad.user_log_dir diff --git a/clint/textui/__init__.py b/clint/textui/__init__.py index aa3a36a7..74113437 100644 --- a/clint/textui/__init__.py +++ b/clint/textui/__init__.py @@ -7,7 +7,13 @@ This module provides the text output helper system. """ - +import sys +if sys.platform.startswith('win'): + from ..packages import colorama + colorama.init() from . import colored -from core import * +from . import progress +from . import prompt + +from .core import * diff --git a/clint/textui/colored.py b/clint/textui/colored.py index dcee9616..2c18b759 100644 --- a/clint/textui/colored.py +++ b/clint/textui/colored.py @@ -14,6 +14,8 @@ import re import sys +PY3 = sys.version_info[0] >= 3 + from ..packages import colorama __all__ = ( @@ -23,10 +25,17 @@ ) COLORS = __all__[:-2] -DISABLE_COLOR = False -if sys.stdout.isatty(): - colorama.init(autoreset=True) +if 'get_ipython' in dir(): + """ + when ipython is fired lot of variables like _oh, etc are used. + There are so many ways to find current python interpreter is ipython. + get_ipython is easiest is most appealing for readers to understand. + """ + DISABLE_COLOR = True +else: + DISABLE_COLOR = False + class ColoredString(object): @@ -36,6 +45,17 @@ def __init__(self, color, s): self.s = s self.color = color + def __getattr__(self, att): + def func_help(*args, **kwargs): + result = getattr(self.s, att)(*args, **kwargs) + if isinstance(result, basestring): + return self._new(result) + elif isinstance(result, list): + return [self._new(x) for x in result] + else: + return result + return func_help + @property def color_str(self): if sys.stdout.isatty() and not DISABLE_COLOR: @@ -47,27 +67,33 @@ def color_str(self): def __len__(self): return len(self.s) - + def __repr__(self): return "<%s-string: '%s'>" % (self.color, self.s) - - def __str__(self): - return self.__unicode__().encode('utf8') - + def __unicode__(self): - return self.color_str - + value = self.color_str + if isinstance(value, bytes): + return value.decode('utf8') + return value + + if PY3: + __str__ = __unicode__ + else: + def __str__(self): + return unicode(self).encode('utf8') + + def __iter__(self): + return iter(self.color_str) + def __add__(self, other): return str(self.color_str) + str(other) - + def __radd__(self, other): return str(other) + str(self.color_str) - + def __mul__(self, other): return (self.color_str * other) - - def split(self, x=' '): - return map(self._new, self.s.split(x)) def _new(self, s): return ColoredString(self.color, s) diff --git a/clint/textui/core.py b/clint/textui/core.py index d782cd0a..c435391e 100644 --- a/clint/textui/core.py +++ b/clint/textui/core.py @@ -13,13 +13,15 @@ import sys -from .progress import progressbar +from contextlib import contextmanager + from .formatters import max_width, min_width from .cols import columns from ..utils import tsplit -__all__ = ('puts', 'puts_err', 'indent', 'progressbar', 'columns', 'max_width', 'min_width') +__all__ = ('puts', 'puts_err', 'indent', 'dedent', 'columns', 'max_width', + 'min_width', 'STDOUT', 'STDERR') STDOUT = sys.stdout.write @@ -27,68 +29,60 @@ NEWLINES = ('\n', '\r', '\r\n') +INDENT_STRINGS = [] +# Private -class Writer(object): - """WriterUtilized by context managers.""" - - shared = dict(indent_level=0, indent_strings=[]) - - - def __init__(self, indent=0, quote='', indent_char=' '): - self.indent = indent - self.indent_char = indent_char - self.indent_quote = quote - if self.indent > 0: - self.indent_string = ''.join(( - str(quote), - (self.indent_char * (indent - len(self.indent_quote))) - )) - else: - self.indent_string = ''.join(( - ('\x08' * (-1 * (indent - len(self.indent_quote)))), - str(quote)) - ) - - if len(self.indent_string): - self.shared['indent_strings'].append(self.indent_string) - - - def __enter__(self): - return self - - - def __exit__(self, type, value, traceback): - self.shared['indent_strings'].pop() - - - def __call__(self, s, newline=True, stream=STDOUT): - - if newline: - s = tsplit(s, NEWLINES) - s = map(str, s) - indent = ''.join(self.shared['indent_strings']) - - s = (str('\n' + indent)).join(s) - - _str = ''.join(( - ''.join(self.shared['indent_strings']), - str(s), - '\n' if newline else '' +def _indent(indent=0, quote='', indent_char=' '): + """Indent util function, compute new indent_string""" + if indent > 0: + indent_string = ''.join(( + str(quote), + (indent_char * (indent - len(quote))) )) - stream(_str) - - -def puts(s, newline=True): - """Prints given string to stdout via Writer interface.""" - Writer()(s, stream=STDOUT) - - -def puts_err(s, newline=True): - """Prints given string to stderr via Writer interface.""" - Writer()(s, stream=STDERR) - + else: + indent_string = ''.join(( + ('\x08' * (-1 * (indent - len(quote)))), + str(quote)) + ) + + if len(indent_string): + INDENT_STRINGS.append(indent_string) + +# Public + +def puts(s='', newline=True, stream=STDOUT): + """Prints given string to stdout.""" + if newline: + s = tsplit(s, NEWLINES) + s = map(str, s) + indent = ''.join(INDENT_STRINGS) + + s = (str('\n' + indent)).join(s) + + _str = ''.join(( + ''.join(INDENT_STRINGS), + str(s), + '\n' if newline else '' + )) + stream(_str) + +def puts_err(s='', newline=True, stream=STDERR): + """Prints given string to stderr.""" + puts(s, newline, stream) + +def dedent(): + """Dedent next strings, use only if you use indent otherwise than as a + context.""" + INDENT_STRINGS.pop() + +@contextmanager +def _indent_context(): + """Indentation context manager.""" + yield + dedent() def indent(indent=4, quote=''): - """Indentation context manager.""" - return Writer(indent=indent, quote=quote) + """Indentation manager, return an indentation context manager.""" + _indent(indent, quote) + return _indent_context() diff --git a/clint/textui/progress.py b/clint/textui/progress.py index a61aba03..1a110c94 100644 --- a/clint/textui/progress.py +++ b/clint/textui/progress.py @@ -11,22 +11,110 @@ from __future__ import absolute_import import sys +import time +STREAM = sys.stderr +# Only show bar in terminals by default (better for piping, logging etc.) +try: + HIDE_DEFAULT = not STREAM.isatty() +except AttributeError: # output does not support isatty() + HIDE_DEFAULT = True -def progressbar(it, prefix='', size=32, hide=False): +BAR_TEMPLATE = '%s[%s%s] %i/%i - %s\r' +MILL_TEMPLATE = '%s %s %i/%i\r' + +DOTS_CHAR = '.' +BAR_FILLED_CHAR = '#' +BAR_EMPTY_CHAR = ' ' +MILL_CHARS = ['|', '/', '-', '\\'] + +#How long to wait before recalculating the ETA +ETA_INTERVAL = 1 +#How many intervals (excluding the current one) to calculate the simple moving average +ETA_SMA_WINDOW = 9 + +def bar(it, label='', width=32, hide=HIDE_DEFAULT, empty_char=BAR_EMPTY_CHAR, filled_char=BAR_FILLED_CHAR, expected_size=None): """Progress iterator. Wrap your iterables with it.""" - count = len(it) + + def _show(_i): + if (time.time() - bar.etadelta) > ETA_INTERVAL: + bar.etadelta = time.time() + bar.ittimes = bar.ittimes[-ETA_SMA_WINDOW:]+[-(bar.start-time.time())/(_i+1)] + bar.eta = sum(bar.ittimes)/float(len(bar.ittimes)) * (count-_i) + bar.etadisp = time.strftime('%H:%M:%S', time.gmtime(bar.eta)) + x = int(width*_i/count) + if not hide: + STREAM.write(BAR_TEMPLATE % ( + label, filled_char*x, empty_char*(width-x), _i, count, bar.etadisp)) + STREAM.flush() + + count = len(it) if expected_size is None else expected_size + + bar.start = time.time() + bar.ittimes = [] + bar.eta = 0 + bar.etadelta = time.time() + bar.etadisp = time.strftime('%H:%M:%S', time.gmtime(bar.eta)) + if count: - def _show(_i): - x = int(size*_i/count) - if not hide: - sys.stdout.write("%s[%s>%s] %i/%i\r" % (prefix, "="*x, "-"*(size-x), _i, count)) - sys.stdout.flush() + _show(0) + + for i, item in enumerate(it): + + yield item + _show(i+1) + + if not hide: + STREAM.write('\n') + STREAM.flush() + + +def dots(it, label='', hide=HIDE_DEFAULT): + """Progress iterator. Prints a dot for each item being iterated""" + count = 0 + + if not hide: + STREAM.write(label) + + for item in it: + if not hide: + STREAM.write(DOTS_CHAR) + sys.stderr.flush() + + count += 1 + + yield item + + STREAM.write('\n') + STREAM.flush() + + +def mill(it, label='', hide=HIDE_DEFAULT, expected_size=None): + """Progress iterator. Prints a mill while iterating over the items.""" + + def _mill_char(_i): + if _i == 100: + return ' ' + else: + return MILL_CHARS[_i % len(MILL_CHARS)] + + def _show(_i): + if not hide: + STREAM.write(MILL_TEMPLATE % ( + label, _mill_char(_i), _i, count)) + STREAM.flush() + + count = len(it) if expected_size is None else expected_size + + if count: _show(0) + for i, item in enumerate(it): + yield item _show(i+1) + if not hide: - sys.stdout.write("\n") - sys.stdout.flush() + STREAM.write('\n') + STREAM.flush() diff --git a/clint/textui/prompt.py b/clint/textui/prompt.py new file mode 100644 index 00000000..7c4bbdd1 --- /dev/null +++ b/clint/textui/prompt.py @@ -0,0 +1,49 @@ +# -*- coding: utf8 -*- + +""" +clint.textui.prompt +~~~~~~~~~~~~~~~~~~~ + +Module for simple interactive prompts handling + +""" + +from __future__ import absolute_import + +from re import match, I + +def yn(prompt, default='y', batch=False): + # A sanity check against default value + # If not y/n then y is assumed + if default not in ['y', 'n']: + default = 'y' + + # Let's build the prompt + choicebox = '[Y/n]' if default == 'y' else '[y/N]' + prompt = prompt + ' ' + choicebox + ' ' + + # If input is not a yes/no variant or empty + # keep asking + while True: + # If batch option is True then auto reply + # with default input + if not batch: + input = raw_input(prompt).strip() + else: + print prompt + input = '' + + # If input is empty default choice is assumed + # so we return True + if input == '': + return True + + # Given 'yes' as input if default choice is y + # then return True, False otherwise + if match('y(?:es)?', input, I): + return True if default == 'y' else False + + # Given 'no' as input if default choice is n + # then return True, False otherwise + elif match('n(?:o)?', input, I): + return True if default == 'n' else False diff --git a/clint/utils.py b/clint/utils.py index 3f499d0b..e84e8514 100644 --- a/clint/utils.py +++ b/clint/utils.py @@ -11,9 +11,33 @@ from __future__ import absolute_import from __future__ import with_statement -import sys import errno +import os.path from os import makedirs +from glob import glob + +try: + basestring +except NameError: + basestring = str + +def expand_path(path): + """Expands directories and globs in given path.""" + + paths = [] + path = os.path.expanduser(path) + path = os.path.expandvars(path) + + if os.path.isdir(path): + + for (dir, dirs, files) in os.walk(path): + for file in files: + paths.append(os.path.join(dir, file)) + else: + paths.extend(glob(path)) + + return paths + def is_collection(obj): @@ -29,7 +53,7 @@ def mkdir_p(path): """Emulates `mkdir -p` behavior.""" try: makedirs(path) - except OSError, exc: # Python >2.5 + except OSError as exc: # Python >2.5 if exc.errno == errno.EEXIST: pass else: @@ -37,17 +61,17 @@ def mkdir_p(path): def tsplit(string, delimiters): """Behaves str.split but supports tuples of delimiters.""" - + delimiters = tuple(delimiters) stack = [string,] - + for delimiter in delimiters: for i, substring in enumerate(stack): substack = substring.split(delimiter) stack.pop(i) for j, _substring in enumerate(substack): stack.insert(i+j, _substring) - + return stack def schunk(string, size): diff --git a/examples/args.py b/examples/args.py index 09301b3b..777aeda5 100644 --- a/examples/args.py +++ b/examples/args.py @@ -16,4 +16,5 @@ puts(colored.red('NOT Files detected: ') + str(args.not_files)) puts(colored.red('Grouped Arguments: ') + str(dict(args.grouped))) -print \ No newline at end of file +print + diff --git a/examples/colors_all.py b/examples/colors_all.py index 92dd7799..79d09c59 100755 --- a/examples/colors_all.py +++ b/examples/colors_all.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import print_function + import sys import os @@ -13,4 +15,4 @@ if __name__ == '__main__': for color in colored.COLORS: - print getattr(colored, color)(text % color.upper()) \ No newline at end of file + print(getattr(colored, color)(text % color.upper())) diff --git a/examples/eng_join.py b/examples/eng_join.py new file mode 100644 index 00000000..12d3deea --- /dev/null +++ b/examples/eng_join.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import os + +sys.path.insert(0, os.path.abspath('..')) + +from clint.eng import join +from clint.textui import colored, indent, puts + +colors = [ + colored.blue('blue'), + colored.red('red'), + colored.yellow('yellow'), + colored.green('green'), + colored.magenta('magenta') +] + +colors = [str(cs) for cs in colors] + + +puts('Smart:') +with indent(4): + for i in range(len(colors)): + puts(join(colors[:i+1])) +puts('\n') +puts('Stupid:') +with indent(4): + for i in range(len(colors)): + puts(join(colors[:i+1], im_a_moron=True, conj='\'n')) diff --git a/examples/get_each_args.py b/examples/get_each_args.py new file mode 100644 index 00000000..b18c7adf --- /dev/null +++ b/examples/get_each_args.py @@ -0,0 +1,13 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +from clint import args +from clint.textui import puts, colored + +all_args = args.grouped + +for item in all_args: + if item is not '_': + puts(colored.red("key:%s"%item)) + print(all_args[item].all) + diff --git a/examples/get_each_args.sh b/examples/get_each_args.sh new file mode 100755 index 00000000..0e940be0 --- /dev/null +++ b/examples/get_each_args.sh @@ -0,0 +1,4 @@ +echo "python get_each_args.py --name kracekumar --email me@kracekumar.com" +python get_each_args.py --name kracekumar --email me@kracekumar.com +echo "python get_each_args.py --languages python c html ruby --email me@kracekumar.com" +python get_each_args.py --langauges python c html ruby --email me@kracekumar.com diff --git a/examples/piped.py b/examples/piped.py index 63701d4e..f0a4ebaf 100644 --- a/examples/piped.py +++ b/examples/piped.py @@ -1,11 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import with_statement + import sys import os -from __future__ import with_statement - sys.path.insert(0, os.path.abspath('..')) from clint import piped_in @@ -23,4 +23,4 @@ with indent(5, quote=colored.red(' |')): puts(in_data) else: - puts(colored.red('Warning: ') + 'No data was piped in.') \ No newline at end of file + puts(colored.red('Warning: ') + 'No data was piped in.') diff --git a/examples/progressbar.py b/examples/progressbar.py index 30cf4045..26030e46 100755 --- a/examples/progressbar.py +++ b/examples/progressbar.py @@ -8,12 +8,20 @@ from time import sleep from random import random -from clint.textui import progressbar +from clint.textui import progress if __name__ == '__main__': - for i in progressbar(range(100)): + for i in progress.bar(range(100)): sleep(random() * 0.2) - - \ No newline at end of file + for i in progress.dots(range(100)): + sleep(random() * 0.2) + + for i in progress.mill(range(100)): + sleep(random() * 0.2) + + # Override the expected_size, for iterables that don't support len() + D = dict(zip(range(100), range(100))) + for k, v in progress.bar(D.iteritems(), expected_size=len(D)): + sleep(random() * 0.2) diff --git a/examples/resources.py b/examples/resources.py index 7380c098..0ec65d0e 100755 --- a/examples/resources.py +++ b/examples/resources.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import print_function + import sys import os @@ -13,16 +15,16 @@ lorem = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' -print '%s created.' % resources.user.path +print('%s created.' % resources.user.path) resources.user.write('lorem.txt', lorem) -print 'lorem.txt created' +print('lorem.txt created') assert resources.user.read('lorem.txt') == lorem -print 'lorem.txt has correct contents' +print('lorem.txt has correct contents') resources.user.delete('lorem.txt') -print 'lorem.txt deleted' +print('lorem.txt deleted') assert resources.user.read('lorem.txt') == None -print 'lorem.txt deletion confirmed' \ No newline at end of file +print('lorem.txt deletion confirmed') diff --git a/examples/text_width.py b/examples/text_width.py index bfbfe153..ed377163 100755 --- a/examples/text_width.py +++ b/examples/text_width.py @@ -19,5 +19,6 @@ col = 60 - puts(columns([(colored.red('Column 1')), col], [(colored.green('Column Two')), None], [(colored.magenta('Column III')), col])) - puts(columns(['hi there my name is kenneth and this is a columns', col], [lorem, None], ['kenneths', col])) \ No newline at end of file + puts(columns([(colored.red('Column 1')), col], [(colored.green('Column Two')), None], + [(colored.magenta('Column III')), col])) + puts(columns(['hi there my name is kenneth and this is a columns', col], [lorem, None], ['kenneths', col])) diff --git a/examples/unicode.json b/examples/unicode.json new file mode 100644 index 00000000..94f9554c --- /dev/null +++ b/examples/unicode.json @@ -0,0 +1,4 @@ +{ + "title": "Bashō's 'old pond'", + "text": "古池や蛙飛込む水の音" +} diff --git a/examples/unicode.py b/examples/unicode.py new file mode 100644 index 00000000..8168fde3 --- /dev/null +++ b/examples/unicode.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import sys +import codecs + +sys.path.insert(0, os.path.abspath('..')) + +try: + import json +except: + import simplejson as json + +from clint import args +from clint import piped_in +from clint.textui import colored, puts, indent + +if __name__ == '__main__': + + puts('Test:') + with indent(4): + puts('%s Fake test 1.' % colored.green('✔')) + puts('%s Fake test 2.' % colored.red('✖')) + + puts('') + puts('Greet:') + with indent(4): + puts(colored.red('Здравствуйте')) + puts(colored.green('你好。')) + puts(colored.yellow('سلام')) + puts(colored.magenta('안녕하세요')) + puts(colored.blue('नमस्ते')) + puts(colored.cyan('γειά σου')) + + puts('') + puts('Arguments:') + with indent(4): + puts('%s' % colored.red(args[0])) + + puts('') + puts('File:') + with indent(4): + f = args.files[0] + puts(colored.yellow('%s:' % f)) + with indent(2): + fd = codecs.open(f, encoding='utf-8') + for line in fd: + line = line.strip('\n\r') + puts(colored.yellow(' %s' % line)) + fd.close() + + puts('') + puts('Input:') + with indent(4): + in_data = json.loads(piped_in()) + title = in_data['title'] + text = in_data['text'] + puts(colored.blue('Title: %s' % title)) + puts(colored.magenta('Text: %s' % text)) diff --git a/examples/unicode.sh b/examples/unicode.sh new file mode 100755 index 00000000..fc93c670 --- /dev/null +++ b/examples/unicode.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +python unicode.py こんにちは。 unicode.json < unicode.json diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index a6f90e78..5cd00a4f --- a/setup.py +++ b/setup.py @@ -4,7 +4,10 @@ import os import sys -from distutils.core import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup import clint @@ -18,7 +21,7 @@ def publish(): publish() sys.exit() -required = [] +required = ['args'] setup( name='clint', @@ -29,6 +32,10 @@ def publish(): author='Kenneth Reitz', author_email='me@kennethreitz.com', url='https://github.com/kennethreitz/clint', + data_files=[ + 'README.rst', + 'HISTORY.rst', + ], packages= [ 'clint', 'clint.textui', @@ -38,14 +45,17 @@ def publish(): license='ISC', classifiers=( # 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', 'Intended Audience :: Developers', 'Natural Language :: English', 'License :: OSI Approved :: ISC License (ISCL)', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', - # 'Programming Language :: Python :: 3.0', - # 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3.2', + 'Topic :: Terminals :: Terminal Emulators/X Terminals', ), ) diff --git a/test_clint.py b/test_clint.py index e9e0b9f0..5bdeda28 100755 --- a/test_clint.py +++ b/test_clint.py @@ -16,5 +16,31 @@ def setUp(self): def tearDown(self): pass +class ColoredStringTestCase(unittest.TestCase): + + def setUp(self): + from clint.textui.colored import ColoredString + + def tearDown(self): + pass + + def test_split(self): + from clint.textui.colored import ColoredString + new_str = ColoredString('red', "hello world") + output = new_str.split() + assert output[0].s == "hello" + + def test_find(self): + from clint.textui.colored import ColoredString + new_str = ColoredString('blue', "hello world") + output = new_str.find('h') + self.assertEqual(output, 0) + + def test_replace(self): + from clint.textui.colored import ColoredString + new_str = ColoredString('green', "hello world") + output = new_str.replace("world", "universe") + assert output.s == "hello universe" + if __name__ == '__main__': unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..24c96a55 --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +[tox] +envlist = py26,py27,py3 + +[testenv] +commands=py.test --junitxml=junit-{envname}.xml +deps = pytest + args + +[testenv:pypy] +basepython=/usr/bin/pypy-c + +[testenv:py3] +basepython=/usr/bin/python3 \ No newline at end of file