├── .codeclimate.yml ├── .gitignore ├── .hues.yml ├── .pyup.yml ├── .travis.yml ├── API Specs.md ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── example-custom.jpg ├── example-simple.jpg ├── index.md └── preview.jpg ├── example.py ├── hues ├── .hues.yml ├── __init__.py ├── colortable.py ├── console.py ├── dpda.py └── huestr.py ├── mkdocs.yml ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── test_colortable.py ├── test_console.py ├── test_dpda.py ├── test_huestr.py └── test_usage.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | pep8: 3 | enabled: true 4 | duplication: 5 | enabled: true 6 | config: 7 | languages: 8 | - python 9 | ratings: 10 | paths: 11 | - "**.py" 12 | exclude_paths: 13 | - '**/tests/**' 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.hues.yml: -------------------------------------------------------------------------------- 1 | labels: 2 | info: INFO 3 | warn: WARNING 4 | error: ERROR 5 | success: SUCCESS 6 | hues: 7 | time: blue 8 | options: 9 | theme: simple 10 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | schedule: "every week" 2 | pr_prefix: "[Bump]" 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.3' 5 | - '3.5' 6 | - pypy 7 | script: python setup.py test 8 | after_success: codeclimate-test-reporter 9 | notifications: 10 | email: false 11 | slack: noop-fa:zbwZeeSi5kgDshj6FO4BkNZU 12 | -------------------------------------------------------------------------------- /API Specs.md: -------------------------------------------------------------------------------- 1 | # Hues Console 2 | 3 | > Notes. 4 | 5 | ## API Considerations, Specs 6 | Console API is borrowed from awesome Node.JS console api. [1] 7 | 8 | ```python 9 | >>> import hues 10 | 11 | >>> hues.log('Look, mama!') # No time, no leaders. 12 | >>> hues.info('Stage 1', 'Operation started', level=0) # Stage 1: Operation Started 13 | >>> hues.info('Stage 1', 'Operation started', level=2) # ----> Stage 1: Operation Started 14 | >>> hues.warn('Uh, hello? Where are my memes?') 15 | >>> hues.abort('I give up!') 16 | >>> hues.error('Mann.', e) 17 | ``` 18 | 19 | ## Default color, theme settings 20 | The default config is a JSON? YAML? file called .hues.[json/yml] in ~ or `cwd`. 21 | 22 | ## tqdm integration 23 | We can ship with a tqdm wrapper. 24 | 25 | 26 | [1]: https://nodejs.org/api/console.html 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | ### Fixed 9 | - Fixed old reference name in a test, which was causing builds to fail. 10 | - Many many readme fixes. 11 | - Test coverage setup. 12 | - Moved codeclimate test reporting to `after_success` so it won't break forks. 13 | 14 | ## [0.2.2] - 2016-10-02 15 | ### Fixed 16 | - Important fix. Setup.py was missing `include_package_data` flag. 17 | 18 | 19 | ## [0.2.1] - 2016-10-02 20 | ### Fixed 21 | - Configuration wasn't being updated. Fixed. 22 | 23 | 24 | ## [0.2.0] - 2016-10-02 25 | ### Added 26 | - Helpers funcs: hues.log, hues.error etc. 27 | - Powerline-ish theme! 28 | - Sane, new API. 29 | 30 | ### Changed 31 | - Added multiple API enhancements. Particularly, enabling shortcut functions. 32 | - Reduced the complexity of the `console` class. 33 | - Configuration is stored in a `.hues.yml` file. 34 | 35 | 36 | ## [0.1.1] - 2016-09-10 37 | ### Fixed 38 | - Missing `MANIFEST.in` caused pip builds to fail.. 39 | - Added `__unicode__` for Python 2. 40 | 41 | ### Added 42 | - Test coverage. 43 | 44 | 45 | ## [0.1.0] - 2016-09-09 46 | ### Added 47 | - Implementation and tests for a deterministic PDA routine helpers. 48 | - Color tables generator. 49 | - Alpha release. 50 | 51 | ### Changed 52 | - Readme updated. 53 | 54 | 55 | ## 0.0.1 - 2016-09-09 56 | ### Added 57 | - Initial project, this CHANGELOG. 58 | - Test skeleton. 59 | - Project skeleton. 60 | 61 | 62 | [UNRELEASED]: https://github.com/prashnts/hues/compare/0.2.1...HEAD 63 | [0.2.2]: https://github.com/prashnts/hues/compare/0.2.1...0.2.2 64 | [0.2.1]: https://github.com/prashnts/hues/compare/0.2.0...0.2.1 65 | [0.2.0]: https://github.com/prashnts/hues/compare/0.1.1...0.2.0 66 | [0.1.1]: https://github.com/prashnts/hues/compare/0.1.0...0.1.1 67 | [0.1.0]: https://github.com/prashnts/hues/compare/0.0.1...0.1.0 68 | 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Prashant Sinha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt 3 | include CHANGELOG.md 4 | include LICENSE 5 | include hues/.hues.yml 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hues 2 | 3 | This is the 90s and your terminal can display _16_ glorious colors. 4 | Your Python scripts deserve the same color love. `Hues` makes printing 5 | to console in color easy. Just grab the package from `PIP`, and your 6 | monochromatic days will be a thing of past! 7 | 8 | 9 | ![Preview](docs/preview.jpg) 10 | 11 | [![Build Status](https://img.shields.io/travis/prashnts/hues/master.svg)](https://travis-ci.org/prashnts/hues) [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/prashnts/hues.svg)](https://codeclimate.com/github/prashnts/hues) [![PyPI](https://img.shields.io/pypi/v/hues.svg)](https://pypi.python.org/pypi/hues) 12 | 13 | ## Quickstart 14 | 15 | Go, grab the latest version from PIP. Run: 16 | 17 | ```bash 18 | pip install hues 19 | ``` 20 | 21 | Then, in your scripts, you can do this: 22 | 23 | ```python 24 | >>> import hues 25 | >>> hues.log('Mission', 42) 26 | >>> hues.info('Finding', 42) 27 | >>> hues.error(41, 'is not', 42) 28 | >>> hues.warn('We are distracted...') 29 | >>> hues.info('Found', 24) 30 | >>> hues.success('Close enough.') 31 | ``` 32 | 33 | ![Example](docs/example-simple.jpg) 34 | 35 | 36 | _whoa!_ 37 | 38 | ### Configuration 39 | 40 | You can add a `.hues.yml` file in your projects, or your home directory, 41 | overriding the defaults. The configuration files are searched and loaded 42 | in this order: 43 | 44 | - Packaged configuration 45 | - User home directory 46 | - Current directory and all the parent directories 47 | 48 | Check out the default configuration [here](hues/.hues.yml). 49 | Currently there's a `powerline` theme shipped with the package which 50 | you can enable by updating `theme` value in configuration. 51 | 52 | 53 | ## Creating your own prompts 54 | 55 | `hues` makes it easy to create your own custom prompt formats with a 56 | Hue String. Hue string is a thin wrapper around Python strings adding 57 | a chainable syntax that's a joy to use! 58 | 59 | ```python 60 | >>> import hues 61 | >>> print(hues.huestr(' 42 ').white.bg_blue.bold.colorized) 62 | ``` 63 | 64 | It does exactly what it says: 65 | 66 | ![Example](docs/example-custom.jpg) 67 | 68 | The Hue string chained attributes use a deterministic pushdown automata 69 | for optimizing the attribute access, so the ANSI escaped strings are 70 | always optimal. 71 | 72 | 73 | ## Colors 74 | 75 | All 16 glorious ANSI colors are available for both background and foreground. Assorted text styles such as **`bold`**, _`italics`_ and `underline` are also available. Too many colors? Worry not fam, go to town with `reset` attribute. 76 | 77 | 78 | ## Todo 79 | - [ ] More Documentation. 80 | - [ ] Unicorns required. 81 | 82 | Please contribute by opening issues, suggestions and patches. 83 | 84 | If you like `hues` or use it in your project, I'd love to hear about it! 85 | Shout at me on [tumblr](//doom.noop.pw) or send me an email. 86 | 87 | 88 | > Back in my days, we didn't even have colors! 89 | -------------------------------------------------------------------------------- /docs/example-custom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prashnts/hues/888049a41e3f2bf33546e53ef3c17494ee8c8790/docs/example-custom.jpg -------------------------------------------------------------------------------- /docs/example-simple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prashnts/hues/888049a41e3f2bf33546e53ef3c17494ee8c8790/docs/example-simple.jpg -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Hues 2 | This is the 90s and your terminal can display _16_ glorious colors. Your Python scripts deserve the some color love. `Hues` makes printing to console in color easy. Just grab the package from `PIP`, and your monochromatic days will be a thing of past! 3 | 4 | 5 | ## Quickstart 6 | 7 | Go, grab the latest version from PIP. Run: 8 | 9 | ```bash 10 | pip install hues 11 | ``` 12 | 13 | Then, in your scripts you can do this: 14 | 15 | ```python 16 | import hues 17 | 18 | hues.info('Destroying the universe') 19 | 20 | try: 21 | destroyinator() 22 | except IncomingPerryThePlatipus: 23 | hues.error('Curse you Perry the Platipus!') 24 | else: 25 | hues.success('Destroyed the universe.') 26 | ``` 27 | 28 | 29 | ## Configuration 30 | 31 | [TODO] 32 | 33 | _whoa!_ 34 | 35 | All the colors, styles and backgrounds are available as object attributes. The chainable syntax is optimized deterministically using a push down automaton, so when you're being particularly indecisive, you can: 36 | 37 | ```python 38 | >>> print(hue('MONDAY!').bold.red.bg_green.underline.bright_yellow) 39 | ``` 40 | 41 | and there won't be a single trace of `red` in your `bright yellow` message to mondays. 42 | 43 | Each `hue` string is self closing, so you can't accidentally color your whole terminal yellow because you forgot the `reset` escape sequence. 44 | 45 | 46 | ## Colors 47 | 48 | All 16 glorious ANSI colors are available for both background and foreground. Assorted text styles such as **`bold`**, _`italics`_ and `underline` are also available. Too many colors? Worry not fam, go to town with `reset` attribute. 49 | 50 | 51 | 52 | > Back in my days, we didn't even have colors! 53 | -------------------------------------------------------------------------------- /docs/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prashnts/hues/888049a41e3f2bf33546e53ef3c17494ee8c8790/docs/preview.jpg -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import hues 2 | import random 3 | import time 4 | 5 | 6 | class ThisPlanetIsProtected(Exception): 7 | pass 8 | 9 | 10 | def destroy_planet(planet): 11 | if planet == 'Earth': 12 | raise ThisPlanetIsProtected 13 | return random.randint(0, 100) < 42 14 | 15 | if __name__ == '__main__': 16 | hues.info('Destroying the planets. Please wait.') 17 | 18 | for planet in ('Murcury', 'Venus', 'Earth', 'Mars', 'Uranus',): 19 | try: 20 | success = destroy_planet(planet) 21 | except ThisPlanetIsProtected: 22 | hues.warn('The Doctor saved', planet) 23 | else: 24 | if success: 25 | hues.success('Destroyed', planet) 26 | else: 27 | hues.error('Could not destroy', planet) 28 | time.sleep(.5) 29 | 30 | hues.info('So long, and thanks for all the fish.') 31 | -------------------------------------------------------------------------------- /hues/.hues.yml: -------------------------------------------------------------------------------- 1 | hues: 2 | default: defaultfg 3 | time: magenta 4 | label: blue 5 | info: cyan 6 | success: green 7 | error: red 8 | warn: yellow 9 | labels: 10 | info: INFO 11 | warn: WARNING 12 | error: ERROR 13 | success: SUCCESS 14 | options: 15 | show_time: yes 16 | time_format: '%H:%M:%S' 17 | 18 | add_newline: yes 19 | 20 | theme: simple 21 | -------------------------------------------------------------------------------- /hues/__init__.py: -------------------------------------------------------------------------------- 1 | # Unicorns 2 | from .huestr import HueString as huestr 3 | from .console import Config, SimpleConsole, PowerlineConsole 4 | 5 | __version__ = (0, 2, 2) 6 | 7 | conf = Config() 8 | 9 | if conf.opts.theme == 'simple': 10 | console = SimpleConsole(conf=conf) 11 | elif conf.opts.theme == 'powerline': 12 | console = PowerlineConsole(conf=conf) 13 | 14 | log = console.log 15 | info = console.info 16 | warn = console.warn 17 | error = console.error 18 | success = console.success 19 | 20 | del conf 21 | 22 | __all__ = ('huestr', 'console', 'log', 'info', 'warn', 'error', 'success') 23 | -------------------------------------------------------------------------------- /hues/colortable.py: -------------------------------------------------------------------------------- 1 | # Unicorns 2 | '''Generate ANSI escape sequences for colors 3 | Source: http://ascii-table.com/ansi-escape-sequences.php 4 | ''' 5 | from collections import namedtuple 6 | 7 | ANSIColors = namedtuple('ANSIColors', [ 8 | 'black', 'red', 'green', 'yellow', 9 | 'blue', 'magenta', 'cyan', 'white', 10 | ]) 11 | ANSIStyles = namedtuple('ANSIStyles', [ 12 | 'reset', 'bold', 'italic', 'underline', 'defaultfg', 'defaultbg', 13 | ]) 14 | 15 | # Style Codes 16 | STYLE = ANSIStyles(0, 1, 3, 4, 39, 49) 17 | 18 | # Regular Colors 19 | FG = ANSIColors(*range(30, 38)) 20 | BG = ANSIColors(*range(40, 48)) 21 | 22 | # High Intensity Colors 23 | HI_FG = ANSIColors(*range(90, 98)) 24 | HI_BG = ANSIColors(*range(100, 108)) 25 | 26 | # Terminal sequence format 27 | SEQ = '\033[%sm' 28 | 29 | 30 | def __gen_keywords__(*args, **kwargs): 31 | '''Helper function to generate single escape sequence mapping.''' 32 | fields = tuple() 33 | values = tuple() 34 | 35 | for tpl in args: 36 | fields += tpl._fields 37 | values += tpl 38 | for prefix, tpl in kwargs.items(): 39 | fields += tuple(map(lambda x: '_'.join([prefix, x]), tpl._fields)) 40 | values += tpl 41 | 42 | return namedtuple('ANSISequences', fields)(*values) 43 | 44 | KEYWORDS = __gen_keywords__(STYLE, FG, bg=BG, bright=HI_FG, bg_bright=HI_BG) 45 | -------------------------------------------------------------------------------- /hues/console.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Unicorns 3 | '''Helper module for all the goodness.''' 4 | import os 5 | import sys 6 | import yaml 7 | from datetime import datetime 8 | from collections import namedtuple 9 | 10 | from .huestr import HueString 11 | from .colortable import KEYWORDS, FG 12 | 13 | if sys.version_info.major == 2: 14 | str = unicode # noqa 15 | 16 | 17 | CONFIG_FNAME = '.hues.yml' 18 | 19 | 20 | class InvalidConfiguration(Exception): 21 | '''Raise when configuration is invalid.''' 22 | 23 | 24 | class Config(object): 25 | def __init__(self, force_default=False): 26 | self.force_default = force_default 27 | self.resolve_config() 28 | 29 | @staticmethod 30 | def load_config(force_default=False): 31 | '''Find and load configuration params. 32 | Config files are loaded in the following order: 33 | - Beginning from current working dir, all the way to the root. 34 | - User home (~). 35 | - Module dir (defaults). 36 | ''' 37 | def _load(cdir, recurse=False): 38 | confl = os.path.join(cdir, CONFIG_FNAME) 39 | try: 40 | with open(confl, 'r') as fp: 41 | conf = yaml.safe_load(fp) 42 | if type(conf) is not dict: 43 | raise InvalidConfiguration('Configuration at %s is not a dictionary.' % confl) 44 | return conf 45 | except EnvironmentError: 46 | parent = os.path.dirname(cdir) 47 | if recurse and parent != cdir: 48 | return _load(parent, recurse=True) 49 | else: 50 | return dict() 51 | except yaml.YAMLError: 52 | raise InvalidConfiguration('Configuration at %s is an invalid YAML file.' % confl) 53 | 54 | def _update(prev, next): 55 | for k, v in next.items(): 56 | if isinstance(v, dict): 57 | prev[k] = _update(prev.get(k, {}), v) 58 | else: 59 | prev[k] = v 60 | return prev 61 | 62 | conf = _load(os.path.dirname(__file__)) 63 | 64 | if not force_default: 65 | home_conf = _load(os.path.expanduser('~')) 66 | local_conf = _load(os.path.abspath(os.curdir), recurse=True) 67 | 68 | _update(conf, home_conf) 69 | _update(conf, local_conf) 70 | return conf 71 | 72 | def resolve_config(self): 73 | '''Resolve configuration params to native instances''' 74 | conf = self.load_config(self.force_default) 75 | for k in conf['hues']: 76 | conf['hues'][k] = getattr(KEYWORDS, conf['hues'][k]) 77 | as_tuples = lambda name, obj: namedtuple(name, obj.keys())(**obj) 78 | 79 | self.hues = as_tuples('Hues', conf['hues']) 80 | self.opts = as_tuples('Options', conf['options']) 81 | self.labels = as_tuples('Labels', conf['labels']) 82 | 83 | 84 | class SimpleConsole(object): 85 | def __init__(self, conf=None, stdout=sys.stdout): 86 | self.stdout = stdout 87 | self.conf = conf if conf else Config() 88 | 89 | def _raw_log(self, *args): 90 | writeout = u''.join([x.colorized for x in args]) 91 | self.stdout.write(writeout) 92 | if self.conf.opts.add_newline: 93 | self.stdout.write('\n') 94 | 95 | def _base_log(self, contents): 96 | def build_component(content, color=None): 97 | fg = KEYWORDS.defaultfg if color is None else color 98 | return ( 99 | HueString(u'{}'.format(content), hue_stack=(fg,)), 100 | HueString(u' - '), 101 | ) 102 | 103 | nargs = () 104 | for content in contents: 105 | if type(content) is tuple and len(content) == 2: 106 | value, color = content 107 | else: 108 | value, color = content, None 109 | nargs += build_component(value, color) 110 | return self._raw_log(*nargs[:-1]) 111 | 112 | def _getTime(self, wrap=None): 113 | time = datetime.now().strftime(self.conf.opts.time_format) 114 | return wrap.format(time) if wrap else time 115 | 116 | def log(self, *args, **kwargs): 117 | nargs = [] 118 | if kwargs.get('time') or ('time' not in kwargs and self.conf.opts.show_time): 119 | nargs.append((self._getTime(), self.conf.hues.time)) 120 | 121 | for k, v in kwargs.items(): 122 | if k in ('info', 'warn', 'error', 'success'): 123 | if v: 124 | label = getattr(self.conf.labels, k) 125 | color = getattr(self.conf.hues, k) 126 | nargs.append((label, color)) 127 | content = u' '.join([str(x) for x in args]) 128 | nargs.append((content, self.conf.hues.default)) 129 | return self._base_log(nargs) 130 | 131 | def info(self, *args): 132 | return self.log(*args, info=True) 133 | 134 | def warn(self, *args): 135 | return self.log(*args, warn=True) 136 | 137 | def error(self, *args): 138 | return self.log(*args, error=True) 139 | 140 | def success(self, *args): 141 | return self.log(*args, success=True) 142 | 143 | def __call__(self, *args): 144 | return self._base_log(args) 145 | 146 | 147 | class PowerlineConsole(SimpleConsole): 148 | def _base_log(self, contents): 149 | def find_fg_color(bg): 150 | if bg >= 90: 151 | bg -= 70 # High intensity background to regular intensity fg. 152 | if bg in (FG.green, FG.yellow, FG.white): 153 | return FG.black 154 | else: 155 | return FG.white 156 | 157 | def build_component(content, color=None, next_fg=None): 158 | fg = KEYWORDS.defaultfg if color is None else color 159 | text_bg = fg + 10 # Background Escape seq offsets by 10. 160 | text_fg = find_fg_color(fg) 161 | next_bg = KEYWORDS.defaultbg if next_fg is None else (next_fg + 10) 162 | 163 | return ( 164 | HueString(u' {} '.format(content), hue_stack=(text_bg, text_fg)), 165 | HueString(u'', hue_stack=(fg, next_bg)), 166 | ) 167 | 168 | nargs = () 169 | 170 | for ix, content in enumerate(contents): 171 | try: 172 | if type(contents[ix + 1]) is tuple: 173 | next_fg = contents[ix + 1][1] 174 | else: 175 | next_fg = None 176 | except IndexError: 177 | next_fg = None 178 | if type(content) is tuple and len(content) == 2: 179 | value, color = content 180 | else: 181 | value, color = content, None 182 | nargs += build_component(value, color, next_fg) 183 | 184 | return self._raw_log(*nargs[:-1]) 185 | -------------------------------------------------------------------------------- /hues/dpda.py: -------------------------------------------------------------------------------- 1 | # Unicorns 2 | '''Deterministic Push Down Automaton helpers. 3 | 4 | This module implements helper functions to allow producing deterministic 5 | representation of arbitrarily chained props. 6 | ''' 7 | from functools import reduce, partial 8 | 9 | 10 | def zero_break(stack): 11 | '''Handle Resets in input stack. 12 | Breaks the input stack if a Reset operator (zero) is encountered. 13 | ''' 14 | reducer = lambda x, y: tuple() if y == 0 else x + (y,) 15 | return reduce(reducer, stack, tuple()) 16 | 17 | 18 | def annihilate(predicate, stack): 19 | '''Squash and reduce the input stack. 20 | Removes the elements of input that match predicate and only keeps the last 21 | match at the end of the stack. 22 | ''' 23 | extra = tuple(filter(lambda x: x not in predicate, stack)) 24 | head = reduce(lambda x, y: y if y in predicate else x, stack, None) 25 | return extra + (head,) if head else extra 26 | 27 | 28 | def annihilator(predicate): 29 | '''Build a partial annihilator for given predicate.''' 30 | return partial(annihilate, predicate) 31 | 32 | 33 | def dedup(stack): 34 | '''Remove duplicates from the stack in first-seen order.''' 35 | # Initializes with an accumulator and then reduces the stack with first match 36 | # deduplication. 37 | reducer = lambda x, y: x if y in x else x + (y,) 38 | return reduce(reducer, stack, tuple()) 39 | 40 | 41 | def apply(funcs, stack): 42 | '''Apply functions to the stack, passing the resulting stack to next state.''' 43 | return reduce(lambda x, y: y(x), funcs, stack) 44 | 45 | 46 | __all__ = ('zero_break', 'annihilator', 'dedup', 'apply') 47 | -------------------------------------------------------------------------------- /hues/huestr.py: -------------------------------------------------------------------------------- 1 | # Unicorns 2 | import sys 3 | from functools import partial 4 | 5 | from .colortable import FG, BG, HI_FG, HI_BG, SEQ, STYLE, KEYWORDS 6 | from .dpda import zero_break, annihilator, dedup, apply 7 | 8 | if sys.version_info.major == 2: 9 | str = unicode # noqa 10 | 11 | 12 | OPTIMIZATION_STEPS = ( 13 | zero_break, # Handle Resets using `reset`. 14 | annihilator(FG + HI_FG), # Squash foreground colors to the last value. 15 | annihilator(BG + HI_BG), # Squash background colors to the last value. 16 | dedup, # Remove duplicates in (remaining) style values. 17 | ) 18 | optimize = partial(apply, OPTIMIZATION_STEPS) 19 | 20 | 21 | def colorize(string, stack): 22 | '''Apply optimal ANSI escape sequences to the string.''' 23 | codes = optimize(stack) 24 | if len(codes): 25 | prefix = SEQ % ';'.join(map(str, codes)) 26 | suffix = SEQ % STYLE.reset 27 | return prefix + string + suffix 28 | else: 29 | return string 30 | 31 | 32 | class HueString(str): 33 | '''Extend the string class to support hues.''' 34 | def __new__(cls, string, hue_stack=None): 35 | '''Return a new instance of the class.''' 36 | return super(HueString, cls).__new__(cls, string) 37 | 38 | def __init__(self, string, hue_stack=tuple()): 39 | self.__string = string 40 | self.__hue_stack = hue_stack 41 | 42 | def __getattr__(self, attr): 43 | try: 44 | code = getattr(KEYWORDS, attr) 45 | hues = self.__hue_stack + (code,) 46 | return HueString(self.__string, hue_stack=hues) 47 | except AttributeError as e: 48 | raise e 49 | 50 | @property 51 | def colorized(self): 52 | return colorize(self.__string, self.__hue_stack) 53 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: hues 2 | site_url: https://hues.noop.pw 3 | repo_url: https://github.com/prashnts/hues/ 4 | site_author: Prashant Sinha 5 | 6 | markdown_extensions: 7 | - smarty 8 | - toc: 9 | permalink: True 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | codeclimate-test-reporter==0.2.3 2 | coverage==4.4.1 3 | extras==1.0.0 4 | fixtures==3.0.0 5 | linecache2==1.0.0 6 | mkdocs==0.16.3 7 | mox3==0.23.0 8 | pbr==3.1.1 9 | py==1.4.34 10 | pyfakefs==3.2 11 | pytest==3.2.1 12 | pytest-cov==2.5.1 13 | pytest-runner==2.12 14 | python-mimeparse==1.6.0 15 | PyYAML==3.12 16 | requests==2.18.4 17 | six==1.10.0 18 | testtools==2.3.0 19 | traceback2==1.4.0 20 | unittest2==1.1.0 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest 3 | 4 | [coverage:run] 5 | source = hues 6 | 7 | [coverage:report] 8 | show_missing = true 9 | exclude_lines = 10 | \# noqa 11 | 12 | [coverage:html] 13 | title = "Hues Test Coverage" 14 | 15 | [tool:pytest] 16 | testpaths = tests 17 | python_files = test_*.py 18 | addopts = --cov 19 | 20 | [pep8] 21 | ignore = E111,E121,E261 22 | max-line-length = 120 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from setuptools import setup 4 | from codecs import open 5 | from os import path 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | # Get the long description from the README file 10 | with open(path.join(here, 'README.md'), encoding='utf-8') as fp: 11 | long_description = fp.read() 12 | 13 | with open(path.join(here, 'hues', '__init__.py'), encoding='utf-8') as fp: 14 | rex = r'^__version__ = \((\d+?), (\d+?), (\d+?)\)$' 15 | vtp = re.search(rex, fp.read(), re.M).groups() 16 | __version__ = '.'.join(vtp) 17 | 18 | install_requires = ('PyYAML',) 19 | setup_requires = ('pytest-runner',) 20 | test_requirements = ['pytest', 'coverage', 'pyfakefs'] 21 | 22 | 23 | setup( 24 | name='hues', 25 | version=__version__, 26 | description='Colored terminal text made easy for Python and happiness.', 27 | long_description=long_description, 28 | url='https://github.com/prashnts/hues', 29 | download_url='https://github.com/prashnts/hues/tarball/' + __version__, 30 | license='MIT', 31 | classifiers=[ 32 | 'Development Status :: 3 - Alpha', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.3', 40 | 'Programming Language :: Python :: 3.4', 41 | 'Topic :: Software Development :: Libraries :: Python Modules' 42 | ], 43 | keywords='color colour terminal text ansi hues unicorns console', 44 | packages=['hues'], 45 | author='Prashant Sinha', 46 | install_requires=install_requires, 47 | setup_requires=setup_requires, 48 | tests_require=test_requirements, 49 | author_email='prashant@noop.pw', 50 | include_package_data=True, 51 | ) 52 | -------------------------------------------------------------------------------- /tests/test_colortable.py: -------------------------------------------------------------------------------- 1 | from hues.colortable import FG, BG, HI_FG, HI_BG, STYLE, SEQ, KEYWORDS 2 | # flake8: noqa 3 | 4 | def test_foreground_colors(): 5 | assert FG.black == 30 6 | assert FG.red == 31 7 | assert FG.green == 32 8 | assert FG.yellow == 33 9 | assert FG.blue == 34 10 | assert FG.magenta == 35 11 | assert FG.cyan == 36 12 | assert FG.white == 37 13 | 14 | def test_background_colors(): 15 | assert BG.black == 40 16 | assert BG.red == 41 17 | assert BG.green == 42 18 | assert BG.yellow == 43 19 | assert BG.blue == 44 20 | assert BG.magenta == 45 21 | assert BG.cyan == 46 22 | assert BG.white == 47 23 | 24 | def test_bright_foreground_colors(): 25 | assert HI_FG.black == 90 26 | assert HI_FG.red == 91 27 | assert HI_FG.green == 92 28 | assert HI_FG.yellow == 93 29 | assert HI_FG.blue == 94 30 | assert HI_FG.magenta == 95 31 | assert HI_FG.cyan == 96 32 | assert HI_FG.white == 97 33 | 34 | def test_bright_background_colors(): 35 | assert HI_BG.black == 100 36 | assert HI_BG.red == 101 37 | assert HI_BG.green == 102 38 | assert HI_BG.yellow == 103 39 | assert HI_BG.blue == 104 40 | assert HI_BG.magenta == 105 41 | assert HI_BG.cyan == 106 42 | assert HI_BG.white == 107 43 | 44 | def test_ansi_styles(): 45 | assert STYLE.reset == 0 46 | assert STYLE.bold == 1 47 | assert STYLE.italic == 3 48 | assert STYLE.underline == 4 49 | assert STYLE.defaultfg == 39 50 | assert STYLE.defaultbg == 49 51 | 52 | def test_sequence(): 53 | assert SEQ % BG.black == '\033[40m' 54 | 55 | def test_keywords(): 56 | assert KEYWORDS.bg_black == BG.black 57 | assert KEYWORDS.bg_bright_black == HI_BG.black 58 | assert KEYWORDS.bold == STYLE.bold 59 | -------------------------------------------------------------------------------- /tests/test_console.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyfakefs.fake_filesystem_unittest as fake_fs_unittest 3 | 4 | try: 5 | from unittest.mock import patch, Mock 6 | except ImportError: 7 | from mock import patch, Mock # noqa 8 | 9 | CONFIG_FNAME = '.hues.yml' 10 | usr_conf = os.path.join(os.path.expanduser('~'), CONFIG_FNAME) 11 | mod_conf = os.path.join(os.path.dirname(__file__), '..', 'hues', CONFIG_FNAME) 12 | 13 | with open(mod_conf, 'r') as fp: 14 | default_conf = fp.read() 15 | 16 | home_conf = ''' 17 | colors: 18 | default: red 19 | ''' 20 | local_conf = ''' 21 | colors: 22 | default: green 23 | ''' 24 | invalid_conf = '''Invalid Conf, but valid YAML.''' 25 | invalid_yaml = '''Nested: Dicts: Are: Invalid!''' 26 | 27 | from hues.console import ( 28 | Config, 29 | InvalidConfiguration, 30 | SimpleConsole, 31 | PowerlineConsole) # noqa 32 | 33 | 34 | class Test_Config(fake_fs_unittest.TestCase): 35 | def setUp(self): 36 | self.setUpPyfakefs() 37 | self.fs.CreateFile(mod_conf, contents=default_conf) 38 | self.fs.CreateFile(usr_conf, contents=home_conf) 39 | self.fs.CreateFile('/var/foo/.hues.yml', contents=local_conf) 40 | self.fs.CreateFile('/var/invalid/.hues.yml', contents=invalid_conf) 41 | self.fs.CreateFile('/var/invalidyml/.hues.yml', contents=invalid_yaml) 42 | self.fs.CreateDirectory('/var/foo/bar/baz') 43 | self.fs.CreateDirectory('/var/doom/baz') 44 | 45 | def test_home_config(self): 46 | os.chdir('/var/doom/baz') 47 | cs = Config.load_config() 48 | assert cs['colors']['default'] == 'red' 49 | 50 | def test_local_config(self): 51 | os.chdir('/var/foo') 52 | cs = Config.load_config() 53 | assert cs['colors']['default'] == 'green' 54 | 55 | def test_local_nested_config(self): 56 | os.chdir('/var/foo/bar/baz') 57 | cs = Config.load_config() 58 | assert cs['colors']['default'] == 'green' 59 | 60 | def test_invalid_config(self): 61 | os.chdir('/var/invalid') 62 | with self.assertRaises(InvalidConfiguration) as e: 63 | Config.load_config() 64 | assert 'not a dictionary' in str(e.exception) 65 | 66 | def test_invalid_yaml(self): 67 | os.chdir('/var/invalidyml') 68 | with self.assertRaises(InvalidConfiguration) as e: 69 | Config.load_config() 70 | assert 'invalid YAML' in str(e.exception) 71 | 72 | 73 | def test_resolved_config(): 74 | cs = Config() 75 | assert hasattr(cs, 'hues') 76 | assert hasattr(cs, 'opts') 77 | assert hasattr(cs.hues, 'default') 78 | 79 | 80 | def test_simple_console(): 81 | console = SimpleConsole(conf=Config()) 82 | for func in ('log', 'info', 'warn', 'error', 'success'): 83 | assert hasattr(console, func) 84 | 85 | 86 | def _get_mock_stdout(): 87 | stdout = Mock() 88 | write = Mock(return_value=None) 89 | stdout.attach_mock(write, 'write') 90 | return stdout, write 91 | 92 | 93 | def test_raw_log_write(): 94 | stdout, write = _get_mock_stdout() 95 | console = SimpleConsole(conf=Config(), stdout=stdout) 96 | console.log('foo') 97 | assert write.call_count == 2 98 | write.assert_any_call('\n') 99 | 100 | assertions = [] 101 | for call in write.mock_calls: 102 | assertions.append(call.called_with.endswith('\033[39mfoo\033[0m')) 103 | assert any(assertions) 104 | 105 | 106 | def test_helpers(): 107 | stdout, write = _get_mock_stdout() 108 | console = SimpleConsole(conf=Config(True), stdout=stdout) 109 | predicate = lambda a, b: lambda x: a in x and b in x 110 | anymatch = lambda colln, predicate: any([predicate(c) for c in colln]) 111 | callargs = lambda x: [str(x) for x in x] 112 | 113 | assert predicate('a', 'b')('oh, hello! we have b.') 114 | assert anymatch(['foo', 'bar'], predicate('ba', 'ar')) 115 | 116 | console.info('alpha') 117 | assert anymatch(callargs(write.mock_calls), predicate('INFO', 'alpha')) 118 | console.error('beta') 119 | assert anymatch(callargs(write.mock_calls), predicate('ERROR', 'beta')) 120 | console.warn('delta') 121 | assert anymatch(callargs(write.mock_calls), predicate('WARN', 'delta')) 122 | console.success('pi') 123 | assert anymatch(callargs(write.mock_calls), predicate('SUCCESS', 'pi')) 124 | 125 | console(('Hey', 30), ('You', 31)) 126 | assert anymatch(callargs(write.mock_calls), predicate('Hey', 'You')) 127 | 128 | 129 | def test_default_color(): 130 | stdout, write = _get_mock_stdout() 131 | console = SimpleConsole(conf=Config(), stdout=stdout) 132 | console('foo') 133 | write.assert_any_call('\033[39mfoo\033[0m') 134 | 135 | 136 | def test_powerline(): 137 | stdout, write = _get_mock_stdout() 138 | console = PowerlineConsole(conf=Config(), stdout=stdout) 139 | console.log('foo', time=False) 140 | write.assert_any_call('\033[49;37m foo \033[0m') 141 | console.warn('foo') 142 | console(('foo', 92), ('bar', 92), 'baz') 143 | -------------------------------------------------------------------------------- /tests/test_dpda.py: -------------------------------------------------------------------------------- 1 | import hues.dpda as DPDA 2 | 3 | def test_zero_negation(): 4 | func = DPDA.zero_break 5 | assert func((1, 2, 3, 4, 0, 10, 1)) == (10, 1) 6 | assert func((1, 2, 3, 4, 5, 0)) == tuple() 7 | 8 | def test_order_annihilation(): 9 | func = DPDA.annihilate 10 | assert func(range(0, 10), (1, 2, 3, 4, 4, 3)) == (3,) 11 | assert func(range(5, 12), (1, 2, 10, 11, 11, 2)) == (1, 2, 2, 11) 12 | 13 | def test_built_order_annihilation(): 14 | f1 = DPDA.annihilator(range(5, 12)) 15 | assert f1((1, 2, 10, 11, 11, 2)) == (1, 2, 2, 11) 16 | 17 | def test_dedup(): 18 | func = DPDA.dedup 19 | assert func((1, 2, 3, 3, 4, 2, 1, 3, 5)) == (1, 2, 3, 4, 5) 20 | 21 | def test_chaining(): 22 | funcs = ( 23 | DPDA.zero_break, # Take the last non-reset subset 24 | DPDA.annihilator(range(5)), # Between 0 and 5, keep the last one 25 | DPDA.annihilator(range(10, 15)), # Between 10 and 15, keep the last one 26 | DPDA.dedup, # Finally remove duplicates 27 | ) 28 | stack = (1, 2, 3, 2, 2, 0, 1, 2, 3, 2, 5, 5, 11, 3, 15, 14) 29 | expected = (5, 15, 3, 14) 30 | 31 | assert DPDA.apply(funcs, stack) == expected 32 | assert DPDA.apply(funcs, (1, 1, 0)) == tuple() 33 | -------------------------------------------------------------------------------- /tests/test_huestr.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from hues.huestr import colorize, HueString 3 | from hues.colortable import FG, BG, STYLE 4 | 5 | if sys.version_info.major == 2: 6 | # Python 2.7 compat. 7 | str = unicode # noqa 8 | 9 | 10 | def test_correct_colorization(): 11 | expected = '\033[30;40municorns\033[0m' 12 | assert colorize('unicorns', (FG.black, BG.black)) == expected 13 | 14 | 15 | def test_optimization(): 16 | stack = (FG.black, FG.red, BG.green, STYLE.bold, STYLE.underline, BG.red) 17 | expected = '\033[1;4;31;41municorns\033[0m' 18 | assert colorize('unicorns', stack) == expected 19 | 20 | 21 | def test_reset(): 22 | stack = (FG.black, STYLE.reset) 23 | expected = 'unicorns' 24 | assert colorize('unicorns', stack) == expected 25 | 26 | 27 | def test_reset_chained(): 28 | stack = (FG.black, BG.black, STYLE.reset, FG.black) 29 | expected = '\033[30municorns\033[0m' 30 | assert colorize('unicorns', stack) == expected 31 | 32 | 33 | def test_hues_creation(): 34 | obj = HueString('woot') 35 | assert isinstance(obj, str) 36 | assert obj.colorized == 'woot' 37 | 38 | 39 | def test_hues_auto_stacking(): 40 | obj = HueString('woot').cyan.bg_green 41 | assert isinstance(obj, str) 42 | assert obj._HueString__hue_stack == (FG.cyan, BG.green) 43 | 44 | 45 | def test_hues_dynamic_props_exceptions(): 46 | try: 47 | HueString('woot').noop 48 | except AttributeError as e: 49 | assert 'noop' in str(e) 50 | else: 51 | raise AssertionError 52 | 53 | 54 | def test_hues_auto_colorize(): 55 | obj = HueString('woot').cyan.bg_green 56 | assert obj.colorized == '\033[36;42mwoot\033[0m' 57 | -------------------------------------------------------------------------------- /tests/test_usage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import hues 3 | from hues import huestr 4 | 5 | 6 | def test_sanity(): 7 | assert type(hues.__version__) is tuple 8 | assert len(hues.__version__) == 3 9 | 10 | 11 | def test_usage(): 12 | assert huestr('unicorn').red.bg_black.colorized == '\033[31;40municorn\033[0m' 13 | --------------------------------------------------------------------------------