├── Pipfile ├── click_tools ├── __init__.py ├── __version__.py ├── pipes.py ├── eng.py ├── utils.py ├── text.py ├── formatters.py ├── cols.py └── resources.py ├── README.rst ├── Pipfile.lock └── setup.py /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | "e1839a8" = {path = ".", editable = true} 10 | click = "*" -------------------------------------------------------------------------------- /click_tools/__init__.py: -------------------------------------------------------------------------------- 1 | from . import eng 2 | from . import pipes 3 | from . import resources 4 | from . import utils 5 | from . import text 6 | from .cols import columns 7 | from .text import * 8 | 9 | import crayons -------------------------------------------------------------------------------- /click_tools/__version__.py: -------------------------------------------------------------------------------- 1 | # dP""b8 88 88 dP""b8 88 dP 888888 dP"Yb dP"Yb 88 .dP"Y8 2 | # dP `" 88 88 dP `" 88odP ________ 88 dP Yb dP Yb 88 `Ybo." 3 | # Yb 88 .o 88 Yb 88"Yb """""""" 88 Yb dP Yb dP 88 .o o.`Y8b 4 | # YboodP 88ood8 88 YboodP 88 Yb 88 YbodP YbodP 88ood8 8bodP' 5 | 6 | __version__ = '0.1.0' -------------------------------------------------------------------------------- /click_tools/pipes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | 6 | 7 | def piped_in(): 8 | """Returns piped input via stdin, else None.""" 9 | with sys.stdin as stdin: 10 | # TTY is only way to detect if stdin contains data 11 | if not stdin.isatty(): 12 | return stdin.read() 13 | else: 14 | return None 15 | 16 | 17 | def is_interactive(): 18 | """Returns if the current session is interactive or not.""" 19 | return bool(os.isatty(sys.stdout.fileno())) 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | click-tools 2 | =========== 3 | 4 | A collection of utilities (extracted from `clint `_), for use with Click. 5 | 6 | Examples 7 | -------- 8 | 9 | Printing various colors:: 10 | 11 | import click 12 | from click_tools import crayons 13 | 14 | click.echo( 15 | '{red}{blue}{gren}'.format( 16 | red=crayon.red('red'), 17 | blue=crayon.blue('blue') 18 | green=crayon.green('green') 19 | ) 20 | ) 21 | 22 | 23 | Identation:: 24 | 25 | from click_tools import puts, indent 26 | 27 | puts('this is an example of text that is not indented') 28 | with indent(4): 29 | puts('This is indented text.') 30 | 31 | 32 | Columns:: 33 | 34 | >>> from click_tools import cols 35 | 36 | >>> a = ( 37 | 'a very long string that requires text-wrapping in order to be ' 38 | 'printed correctly.' 39 | ) 40 | >>> b = 'this is other text\nothertext\nothertext' 41 | 42 | >>> click.echo(columns((a, 20), (b, 20), (b, None))) 43 | a very long string this is other text this is other text 44 | that requires othertext othertext 45 | text-wrapping in othertext othertext 46 | order to be printed 47 | correctly. 48 | -------------------------------------------------------------------------------- /click_tools/eng.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | clint.eng 5 | ~~~~~~~~~ 6 | 7 | This module provides English language string helpers. 8 | 9 | """ 10 | from __future__ import print_function 11 | 12 | COMMA = ',' 13 | CONJUNCTION = 'and' 14 | SPACE = ' ' 15 | 16 | try: 17 | unicode 18 | except NameError: 19 | unicode = str 20 | 21 | 22 | def join(l, conj=CONJUNCTION, oxford=True, separator=COMMA): 23 | """Joins lists of words. Oxford comma and all.""" 24 | 25 | im_a_moron = (not oxford) 26 | 27 | collector = [] 28 | left = len(l) 29 | separator = separator + SPACE 30 | conj = conj + SPACE 31 | 32 | for _l in l[:]: 33 | 34 | left += -1 35 | 36 | collector.append(_l) 37 | if left == 1: 38 | if len(l) == 2 or im_a_moron: 39 | collector.append(SPACE) 40 | else: 41 | collector.append(separator) 42 | 43 | collector.append(conj) 44 | 45 | elif left is not 0: 46 | collector.append(separator) 47 | 48 | return unicode(str().join(collector)) 49 | 50 | 51 | if __name__ == '__main__': 52 | print(join(['blue', 'red', 'yellow'], conj='or', oxford=False)) 53 | print(join(['blue', 'red', 'yellow'], conj='or')) 54 | print(join(['blue', 'red'], conj='or')) 55 | print(join(['blue', 'red'], conj='and')) 56 | print(join(['blue'], conj='and')) 57 | print(join(['blue', 'red', 'yellow', 'green', 'yello'], conj='and')) 58 | -------------------------------------------------------------------------------- /click_tools/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | click-tools.utils 5 | ~~~~~~~~~~~~~~~~~ 6 | 7 | Various Python helpers used within click-tools. 8 | 9 | """ 10 | 11 | from __future__ import absolute_import 12 | from __future__ import with_statement 13 | 14 | import errno 15 | import os.path 16 | from os import makedirs 17 | from glob import glob 18 | 19 | try: 20 | basestring 21 | except NameError: 22 | basestring = str 23 | 24 | 25 | def expand_path(path): 26 | """Expands directories and globs in given path.""" 27 | 28 | paths = [] 29 | path = os.path.expanduser(path) 30 | path = os.path.expandvars(path) 31 | 32 | if os.path.isdir(path): 33 | 34 | for (dir, dirs, files) in os.walk(path): 35 | for file in files: 36 | paths.append(os.path.join(dir, file)) 37 | else: 38 | paths.extend(glob(path)) 39 | 40 | return paths 41 | 42 | 43 | def is_collection(obj): 44 | """Tests if an object is a collection. Strings don't count.""" 45 | 46 | if isinstance(obj, basestring): 47 | return False 48 | 49 | return hasattr(obj, '__getitem__') 50 | 51 | 52 | def mkdir_p(path): 53 | """Emulates `mkdir -p` behavior.""" 54 | try: 55 | makedirs(path) 56 | except OSError as exc: # Python >2.5 57 | if exc.errno == errno.EEXIST: 58 | pass 59 | else: 60 | raise 61 | 62 | 63 | def tsplit(string, delimiters): 64 | """Behaves str.split but supports tuples of delimiters.""" 65 | delimiters = tuple(delimiters) 66 | if len(delimiters) < 1: 67 | return [string, ] 68 | final_delimiter = delimiters[0] 69 | for i in delimiters[1:]: 70 | string = string.replace(i, final_delimiter) 71 | return string.split(final_delimiter) 72 | 73 | 74 | def schunk(string, size): 75 | """Splits string into n sized chunks.""" 76 | return [string[i:i + size] for i in range(0, len(string), size)] 77 | -------------------------------------------------------------------------------- /click_tools/text.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import sys 4 | 5 | from contextlib import contextmanager 6 | 7 | from .formatters import max_width, min_width 8 | from .cols import columns 9 | from .utils import tsplit 10 | 11 | 12 | __all__ = ( 13 | 'puts', 'puts_err', 'indent', 'dedent', 'columns', 'max_width', 14 | 'min_width', 'STDOUT', 'STDERR' 15 | ) 16 | 17 | 18 | STDOUT = sys.stdout.write 19 | STDERR = sys.stderr.write 20 | 21 | NEWLINES = ('\n', '\r', '\r\n') 22 | 23 | INDENT_STRINGS = [] 24 | 25 | # Private ------ 26 | 27 | 28 | def _indent(indent=0, quote='', indent_char=' '): 29 | """Indent util function, compute new indent_string""" 30 | if indent > 0: 31 | indent_string = ''.join(( 32 | str(quote), 33 | (indent_char * (indent - len(quote))) 34 | )) 35 | else: 36 | indent_string = ''.join(( 37 | ('\x08' * (-1 * (indent - len(quote)))), 38 | str(quote)) 39 | ) 40 | 41 | if len(indent_string): 42 | INDENT_STRINGS.append(indent_string) 43 | 44 | # Public ------ 45 | 46 | 47 | def puts(s='', newline=True, stream=STDOUT): 48 | """Prints given string to stdout.""" 49 | if newline: 50 | s = tsplit(s, NEWLINES) 51 | s = map(str, s) 52 | indent = ''.join(INDENT_STRINGS) 53 | 54 | s = (str('\n' + indent)).join(s) 55 | 56 | _str = ''.join(( 57 | ''.join(INDENT_STRINGS), 58 | str(s), 59 | '\n' if newline else '' 60 | )) 61 | stream(_str) 62 | 63 | 64 | def puts_err(s='', newline=True, stream=STDERR): 65 | """Prints given string to stderr.""" 66 | puts(s, newline, stream) 67 | 68 | 69 | def dedent(): 70 | """Dedent next strings, use only if you use indent otherwise than as a 71 | context.""" 72 | INDENT_STRINGS.pop() 73 | 74 | 75 | @contextmanager 76 | def _indent_context(): 77 | """Indentation context manager.""" 78 | try: 79 | yield 80 | finally: 81 | dedent() 82 | 83 | 84 | def indent(indent=4, quote=''): 85 | """Indentation manager, return an indentation context manager.""" 86 | _indent(indent, quote) 87 | return _indent_context() 88 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "33cb85232dceab06582cd0ef5882cb391e8f6f3adc7242e1260489a7ba879bf1" 5 | }, 6 | "host-environment-markers": { 7 | "implementation_name": "cpython", 8 | "implementation_version": "3.6.2", 9 | "os_name": "posix", 10 | "platform_machine": "x86_64", 11 | "platform_python_implementation": "CPython", 12 | "platform_release": "16.7.0", 13 | "platform_system": "Darwin", 14 | "platform_version": "Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64", 15 | "python_full_version": "3.6.2", 16 | "python_version": "3.6", 17 | "sys_platform": "darwin" 18 | }, 19 | "pipfile-spec": 6, 20 | "requires": {}, 21 | "sources": [ 22 | { 23 | "name": "pypi", 24 | "url": "https://pypi.python.org/simple", 25 | "verify_ssl": true 26 | } 27 | ] 28 | }, 29 | "default": { 30 | "appdirs": { 31 | "hashes": [ 32 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e", 33 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92" 34 | ], 35 | "version": "==1.4.3" 36 | }, 37 | "click": { 38 | "hashes": [ 39 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", 40 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" 41 | ], 42 | "version": "==6.7" 43 | }, 44 | "colorama": { 45 | "hashes": [ 46 | "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", 47 | "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" 48 | ], 49 | "version": "==0.3.9" 50 | }, 51 | "crayons": { 52 | "hashes": [ 53 | "sha256:6f51241d0c4faec1c04c1c0ac6a68f1d66a4655476ce1570b3f37e5166a599cc", 54 | "sha256:5e17691605e564d63482067eb6327d01a584bbaf870beffd4456a3391bd8809d" 55 | ], 56 | "version": "==0.1.2" 57 | }, 58 | "e1839a8": { 59 | "editable": true, 60 | "path": "." 61 | } 62 | }, 63 | "develop": {} 64 | } 65 | -------------------------------------------------------------------------------- /click_tools/formatters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | clint.textui.formatters 5 | ~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Core TextUI functionality for text formatting. 8 | 9 | """ 10 | 11 | from __future__ import absolute_import 12 | 13 | from crayons import ColoredString, clean 14 | from .utils import tsplit, schunk 15 | 16 | 17 | NEWLINES = ('\n', '\r', '\r\n') 18 | 19 | 20 | def min_width(string, cols, padding=' '): 21 | """Returns given string with right padding.""" 22 | 23 | is_color = isinstance(string, ColoredString) 24 | 25 | stack = tsplit(str(string), NEWLINES) 26 | 27 | for i, substring in enumerate(stack): 28 | _sub = clean(substring).ljust((cols + 0), padding) 29 | if is_color: 30 | _sub = (_sub.replace(clean(substring), substring)) 31 | stack[i] = _sub 32 | 33 | return '\n'.join(stack) 34 | 35 | 36 | def max_width(string, cols, separator='\n'): 37 | """Returns a freshly formatted 38 | :param string: string to be formatted 39 | :type string: basestring or clint.textui.colored.ColoredString 40 | :param cols: max width the text to be formatted 41 | :type cols: int 42 | :param separator: separator to break rows 43 | :type separator: basestring 44 | 45 | >>> formatters.max_width('123 5678', 8) 46 | '123 5678' 47 | >>> formatters.max_width('123 5678', 7) 48 | '123 \n5678' 49 | 50 | """ 51 | 52 | is_color = isinstance(string, ColoredString) 53 | 54 | if is_color: 55 | string_copy = string._new('') 56 | string = string.s 57 | 58 | stack = tsplit(string, NEWLINES) 59 | 60 | for i, substring in enumerate(stack): 61 | stack[i] = substring.split() 62 | 63 | _stack = [] 64 | 65 | for row in stack: 66 | _row = ['', ] 67 | _row_i = 0 68 | 69 | for word in row: 70 | if (len(_row[_row_i]) + len(word)) <= cols: 71 | _row[_row_i] += word 72 | _row[_row_i] += ' ' 73 | 74 | elif len(word) > cols: 75 | 76 | # ensure empty row 77 | if len(_row[_row_i]): 78 | _row[_row_i] = _row[_row_i].rstrip() 79 | _row.append('') 80 | _row_i += 1 81 | 82 | chunks = schunk(word, cols) 83 | for i, chunk in enumerate(chunks): 84 | if not (i + 1) == len(chunks): 85 | _row[_row_i] += chunk 86 | _row[_row_i] = _row[_row_i].rstrip() 87 | _row.append('') 88 | _row_i += 1 89 | else: 90 | _row[_row_i] += chunk 91 | _row[_row_i] += ' ' 92 | else: 93 | _row[_row_i] = _row[_row_i].rstrip() 94 | _row.append('') 95 | _row_i += 1 96 | _row[_row_i] += word 97 | _row[_row_i] += ' ' 98 | else: 99 | _row[_row_i] = _row[_row_i].rstrip() 100 | 101 | _row = map(str, _row) 102 | _stack.append(separator.join(_row)) 103 | 104 | _s = '\n'.join(_stack) 105 | if is_color: 106 | _s = string_copy._new(_s) 107 | return _s 108 | -------------------------------------------------------------------------------- /click_tools/cols.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | clint.textui.columns 5 | ~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Core TextUI functionality for column formatting. 8 | 9 | """ 10 | 11 | from __future__ import absolute_import 12 | 13 | from .formatters import max_width, min_width 14 | from .utils import tsplit 15 | 16 | import sys 17 | 18 | 19 | NEWLINES = ('\n', '\r', '\r\n') 20 | 21 | 22 | 23 | def _find_unix_console_width(): 24 | import termios, fcntl, struct, sys 25 | 26 | # fcntl.ioctl will fail if stdout is not a tty 27 | if not sys.stdout.isatty(): 28 | return None 29 | 30 | s = struct.pack("HHHH", 0, 0, 0, 0) 31 | fd_stdout = sys.stdout.fileno() 32 | size = fcntl.ioctl(fd_stdout, termios.TIOCGWINSZ, s) 33 | height, width = struct.unpack("HHHH", size)[:2] 34 | return width 35 | 36 | 37 | def _find_windows_console_width(): 38 | # http://code.activestate.com/recipes/440694/ 39 | from ctypes import windll, create_string_buffer 40 | STDIN, STDOUT, STDERR = -10, -11, -12 41 | 42 | h = windll.kernel32.GetStdHandle(STDERR) 43 | csbi = create_string_buffer(22) 44 | res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) 45 | 46 | if res: 47 | import struct 48 | (bufx, bufy, curx, cury, wattr, 49 | left, top, right, bottom, 50 | maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) 51 | sizex = right - left + 1 52 | sizey = bottom - top + 1 53 | return sizex 54 | 55 | 56 | def console_width(kwargs): 57 | """"Determine console_width.""" 58 | 59 | if sys.platform.startswith('win'): 60 | console_width = _find_windows_console_width() 61 | else: 62 | console_width = _find_unix_console_width() 63 | 64 | _width = kwargs.get('width', None) 65 | if _width: 66 | console_width = _width 67 | else: 68 | if not console_width: 69 | console_width = 80 70 | 71 | return console_width 72 | 73 | 74 | 75 | def columns(*cols, **kwargs): 76 | 77 | columns = list(cols) 78 | 79 | cwidth = console_width(kwargs) 80 | 81 | _big_col = None 82 | _total_cols = 0 83 | 84 | cols = [list(c) for c in cols] 85 | 86 | for i, (string, width) in enumerate(cols): 87 | 88 | if width is not None: 89 | _total_cols += (width + 1) 90 | cols[i][0] = max_width(string, width).split('\n') 91 | else: 92 | _big_col = i 93 | 94 | if _big_col: 95 | cols[_big_col][1] = (cwidth - _total_cols) - len(cols) 96 | cols[_big_col][0] = max_width(cols[_big_col][0], cols[_big_col][1]).split('\n') 97 | 98 | height = len(max([c[0] for c in cols], key=len)) 99 | 100 | for i, (strings, width) in enumerate(cols): 101 | 102 | for _ in range(height - len(strings)): 103 | cols[i][0].append('') 104 | 105 | for j, string in enumerate(strings): 106 | cols[i][0][j] = min_width(string, width) 107 | 108 | stack = [c[0] for c in cols] 109 | _out = [] 110 | 111 | for i in range(height): 112 | _row = '' 113 | 114 | for col in stack: 115 | _row += col[i] 116 | _row += ' ' 117 | 118 | _out.append(_row) 119 | 120 | 121 | 122 | return '\n'.join(_out) 123 | 124 | 125 | 126 | ########################### 127 | if __name__ == '__main__': 128 | a = 'this is text that goes into a small column\n cool?' 129 | b = 'this is other text\nothertext\nothertext' 130 | 131 | print(columns((a, 10), (b, 20), (b, None))) 132 | 133 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | import sys 10 | from shutil import rmtree 11 | 12 | from setuptools import find_packages, setup, Command 13 | 14 | # Package meta-data. 15 | NAME = 'click_tools' 16 | DESCRIPTION = 'A collection of tools for command-line applications.' 17 | URL = 'https://github.com/kennethreitz/click-tools' 18 | EMAIL = 'me@kennethreitz.org' 19 | AUTHOR = 'Kenneth Reitz' 20 | 21 | # What packages are required for this module to be executed? 22 | REQUIRED = [ 23 | 'appdirs', 'crayons' 24 | ] 25 | 26 | # The rest you shouldn't have to touch too much :) 27 | # ------------------------------------------------ 28 | # Except, perhaps the License and Trove Classifiers! 29 | # If you do change the License, remember to change the Trove Classifier for that! 30 | 31 | here = os.path.abspath(os.path.dirname(__file__)) 32 | 33 | # Import the README and use it as the long-description. 34 | # Note: this will only work if 'README.rst' is present in your MANIFEST.in file! 35 | with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 36 | long_description = '\n' + f.read() 37 | 38 | # Load the package's __version__.py module as a dictionary. 39 | about = {} 40 | with open(os.path.join(here, NAME, '__version__.py')) as f: 41 | exec(f.read(), about) 42 | 43 | 44 | class UploadCommand(Command): 45 | """Support setup.py upload.""" 46 | 47 | description = 'Build and publish the package.' 48 | user_options = [] 49 | 50 | @staticmethod 51 | def status(s): 52 | """Prints things in bold.""" 53 | print('\033[1m{0}\033[0m'.format(s)) 54 | 55 | def initialize_options(self): 56 | pass 57 | 58 | def finalize_options(self): 59 | pass 60 | 61 | def run(self): 62 | try: 63 | self.status('Removing previous builds…') 64 | rmtree(os.path.join(here, 'dist')) 65 | except OSError: 66 | pass 67 | 68 | self.status('Building Source and Wheel (universal) distribution…') 69 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 70 | 71 | self.status('Uploading the package to PyPi via Twine…') 72 | os.system('twine upload dist/*') 73 | 74 | sys.exit() 75 | 76 | 77 | # Where the magic happens: 78 | setup( 79 | name=NAME, 80 | version=about['__version__'], 81 | description=DESCRIPTION, 82 | long_description=long_description, 83 | author=AUTHOR, 84 | author_email=EMAIL, 85 | url=URL, 86 | packages=find_packages(exclude=('tests',)), 87 | # If your package is a single module, use this instead of 'packages': 88 | # py_modules=['mypackage'], 89 | 90 | # entry_points={ 91 | # 'console_scripts': ['mycli=mymodule:cli'], 92 | # }, 93 | install_requires=REQUIRED, 94 | include_package_data=True, 95 | license='MIT', 96 | classifiers=[ 97 | # Trove classifiers 98 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 99 | 'License :: OSI Approved :: MIT License', 100 | 'Programming Language :: Python', 101 | 'Programming Language :: Python :: 2.6', 102 | 'Programming Language :: Python :: 2.7', 103 | 'Programming Language :: Python :: 3', 104 | 'Programming Language :: Python :: 3.3', 105 | 'Programming Language :: Python :: 3.4', 106 | 'Programming Language :: Python :: 3.5', 107 | 'Programming Language :: Python :: 3.6', 108 | 'Programming Language :: Python :: Implementation :: CPython', 109 | 'Programming Language :: Python :: Implementation :: PyPy' 110 | ], 111 | # $ setup.py publish support. 112 | cmdclass={ 113 | 'upload': UploadCommand, 114 | }, 115 | ) -------------------------------------------------------------------------------- /click_tools/resources.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | click-tools.resources 5 | ~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | This module contains all the application resource features of click-tools. 8 | 9 | """ 10 | 11 | 12 | from __future__ import absolute_import 13 | from __future__ import with_statement 14 | 15 | import errno 16 | from os import remove, removedirs 17 | from os.path import isfile, join as path_join 18 | 19 | from appdirs import AppDirs 20 | 21 | from .utils import mkdir_p, is_collection 22 | 23 | 24 | __all__ = ( 25 | 'init', 'user', 'site', 'cache', 26 | 'log', 'NotConfigured' 27 | ) 28 | 29 | 30 | class AppDir(object): 31 | """Application Directory object.""" 32 | 33 | def __init__(self, path=None): 34 | self.path = path 35 | self._exists = False 36 | 37 | if path: 38 | self._create() 39 | 40 | def __repr__(self): 41 | return '' % (self.path) 42 | 43 | def __getattribute__(self, name): 44 | 45 | if not name in ('_exists', 'path', '_create', '_raise_if_none'): 46 | if not self._exists: 47 | self._create() 48 | return object.__getattribute__(self, name) 49 | 50 | def _raise_if_none(self): 51 | """Raises if operations are carried out on an unconfigured AppDir.""" 52 | if not self.path: 53 | raise NotConfigured() 54 | 55 | def _create(self): 56 | """Creates current AppDir at AppDir.path.""" 57 | 58 | self._raise_if_none() 59 | if not self._exists: 60 | mkdir_p(self.path) 61 | self._exists = True 62 | 63 | def open(self, filename, mode='r'): 64 | """Returns file object from given filename.""" 65 | 66 | self._raise_if_none() 67 | fn = path_join(self.path, filename) 68 | 69 | return open(fn, mode) 70 | 71 | def write(self, filename, content, binary=False): 72 | """Writes given content to given filename.""" 73 | self._raise_if_none() 74 | fn = path_join(self.path, filename) 75 | 76 | if binary: 77 | flags = 'wb' 78 | else: 79 | flags = 'w' 80 | 81 | with open(fn, flags) as f: 82 | f.write(content) 83 | 84 | def append(self, filename, content, binary=False): 85 | """Appends given content to given filename.""" 86 | 87 | self._raise_if_none() 88 | fn = path_join(self.path, filename) 89 | 90 | if binary: 91 | flags = 'ab' 92 | else: 93 | flags = 'a' 94 | 95 | with open(fn, flags) as f: 96 | f.write(content) 97 | return True 98 | 99 | def delete(self, filename=''): 100 | """Deletes given file or directory. If no filename is passed, current 101 | directory is removed. 102 | """ 103 | self._raise_if_none() 104 | fn = path_join(self.path, filename) 105 | 106 | try: 107 | if isfile(fn): 108 | remove(fn) 109 | else: 110 | removedirs(fn) 111 | except OSError as why: 112 | if why.errno == errno.ENOENT: 113 | pass 114 | else: 115 | raise why 116 | 117 | def read(self, filename, binary=False): 118 | """Returns contents of given file with AppDir. 119 | If file doesn't exist, returns None.""" 120 | 121 | self._raise_if_none() 122 | fn = path_join(self.path, filename) 123 | 124 | if binary: 125 | flags = 'br' 126 | else: 127 | flags = 'r' 128 | 129 | try: 130 | with open(fn, flags) as f: 131 | return f.read() 132 | except IOError: 133 | return None 134 | 135 | 136 | def sub(self, path): 137 | """Returns AppDir instance for given subdirectory name.""" 138 | 139 | if is_collection(path): 140 | path = path_join(path) 141 | 142 | return AppDir(path_join(self.path, path)) 143 | 144 | 145 | # Module locals 146 | 147 | user = AppDir() 148 | site = AppDir() 149 | cache = AppDir() 150 | log = AppDir() 151 | 152 | 153 | def init(vendor, name): 154 | 155 | global user, site, cache, log 156 | 157 | ad = AppDirs(name, vendor) 158 | 159 | user.path = ad.user_data_dir 160 | 161 | site.path = ad.site_data_dir 162 | cache.path = ad.user_cache_dir 163 | log.path = ad.user_log_dir 164 | 165 | 166 | class NotConfigured(IOError): 167 | """Application configuration required. Please run resources.init() first.""" 168 | --------------------------------------------------------------------------------