├── .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 | 
10 |
11 | [](https://travis-ci.org/prashnts/hues) [](https://codeclimate.com/github/prashnts/hues) [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------