cli2 5.2.2__tar.gz → 6.0.0rc4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. cli2-6.0.0rc4/PKG-INFO +34 -0
  2. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/__init__.py +16 -4
  3. cli2-6.0.0rc4/cli2/asyncio.py +65 -0
  4. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/cli.py +76 -49
  5. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/configuration.py +6 -3
  6. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/display.py +15 -4
  7. cli2-6.0.0rc4/cli2/examples/traceback_demo.py +205 -0
  8. cli2-6.0.0rc4/cli2/exceptions.py +28 -0
  9. cli2-6.0.0rc4/cli2/file.py +241 -0
  10. cli2-6.0.0rc4/cli2/find.py +146 -0
  11. cli2-6.0.0rc4/cli2/flow2.py +12 -0
  12. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/interactive.py +33 -17
  13. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/log.py +15 -1
  14. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/proc.py +14 -74
  15. cli2-5.2.2/cli2/asyncio.py → cli2-6.0.0rc4/cli2/queue.py +12 -41
  16. cli2-6.0.0rc4/cli2/template2.py +13 -0
  17. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/theme.py +8 -7
  18. cli2-6.0.0rc4/cli2/traceback.py +454 -0
  19. cli2-6.0.0rc4/cli2.egg-info/PKG-INFO +34 -0
  20. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2.egg-info/SOURCES.txt +9 -25
  21. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2.egg-info/entry_points.txt +8 -2
  22. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2.egg-info/requires.txt +2 -0
  23. cli2-6.0.0rc4/pyproject.toml +3 -0
  24. {cli2-5.2.2 → cli2-6.0.0rc4}/setup.py +11 -3
  25. cli2-5.2.2/PKG-INFO +0 -69
  26. cli2-5.2.2/README.rst +0 -31
  27. cli2-5.2.2/classifiers.txt +0 -10
  28. cli2-5.2.2/cli2.egg-info/PKG-INFO +0 -69
  29. cli2-5.2.2/tests/test_ansible.py +0 -222
  30. cli2-5.2.2/tests/test_ansible_variables.py +0 -46
  31. cli2-5.2.2/tests/test_asyncio.py +0 -53
  32. cli2-5.2.2/tests/test_cli.py +0 -85
  33. cli2-5.2.2/tests/test_client.py +0 -1358
  34. cli2-5.2.2/tests/test_client_test.py +0 -21
  35. cli2-5.2.2/tests/test_command.py +0 -724
  36. cli2-5.2.2/tests/test_configuration.py +0 -80
  37. cli2-5.2.2/tests/test_decorators.py +0 -114
  38. cli2-5.2.2/tests/test_display.py +0 -72
  39. cli2-5.2.2/tests/test_entry_point.py +0 -16
  40. cli2-5.2.2/tests/test_group.py +0 -255
  41. cli2-5.2.2/tests/test_inject.py +0 -120
  42. cli2-5.2.2/tests/test_interactive.py +0 -50
  43. cli2-5.2.2/tests/test_lock.py +0 -44
  44. cli2-5.2.2/tests/test_log.py +0 -80
  45. cli2-5.2.2/tests/test_mask.py +0 -30
  46. cli2-5.2.2/tests/test_node.py +0 -67
  47. cli2-5.2.2/tests/test_notlevenshtein.py +0 -46
  48. cli2-5.2.2/tests/test_proc.py +0 -150
  49. cli2-5.2.2/tests/test_prompt2.py +0 -210
  50. cli2-5.2.2/tests/test_restful.py +0 -95
  51. cli2-5.2.2/tests/test_table.py +0 -130
  52. {cli2-5.2.2 → cli2-6.0.0rc4}/MANIFEST.in +0 -0
  53. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/cli2.py +0 -0
  54. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/colors.py +0 -0
  55. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/decorators.py +0 -0
  56. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/examples/__init__.py +0 -0
  57. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/examples/conf.py +0 -0
  58. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/examples/example.py +0 -0
  59. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/examples/example_obj.py +0 -0
  60. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/examples/nesting.py +0 -0
  61. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/examples/obj.py +0 -0
  62. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/examples/obj2.py +0 -0
  63. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/examples/test.py +0 -0
  64. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/lock.py +0 -0
  65. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/mask.py +0 -0
  66. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/node.py +0 -0
  67. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/notlevenshtein.py +0 -0
  68. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/sphinx.py +0 -0
  69. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/table.py +0 -0
  70. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2/test.py +0 -0
  71. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2.egg-info/dependency_links.txt +0 -0
  72. {cli2-5.2.2 → cli2-6.0.0rc4}/cli2.egg-info/top_level.txt +0 -0
  73. {cli2-5.2.2 → cli2-6.0.0rc4}/setup.cfg +0 -0
cli2-6.0.0rc4/PKG-INFO ADDED
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli2
3
+ Version: 6.0.0rc4
4
+ Home-page: https://yourlabs.io/oss/cli2
5
+ Author: James Pic
6
+ Author-email: jamespic@gmail.com
7
+ License: MIT
8
+ Keywords: cli
9
+ Requires-Python: >=3.6
10
+ Requires-Dist: docstring_parser
11
+ Requires-Dist: pyyaml
12
+ Requires-Dist: pygments
13
+ Requires-Dist: structlog
14
+ Requires-Dist: aiofiles
15
+ Provides-Extra: httpx
16
+ Requires-Dist: chttpx; extra == "httpx"
17
+ Provides-Extra: ansible
18
+ Requires-Dist: cansible; extra == "ansible"
19
+ Provides-Extra: test
20
+ Requires-Dist: freezegun; extra == "test"
21
+ Requires-Dist: pytest; extra == "test"
22
+ Requires-Dist: pytest-cov; extra == "test"
23
+ Requires-Dist: pytest-mock; extra == "test"
24
+ Requires-Dist: pytest-asyncio; extra == "test"
25
+ Requires-Dist: pytest-httpx; extra == "test"
26
+ Requires-Dist: pytest-env; extra == "test"
27
+ Dynamic: author
28
+ Dynamic: author-email
29
+ Dynamic: home-page
30
+ Dynamic: keywords
31
+ Dynamic: license
32
+ Dynamic: provides-extra
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
@@ -1,4 +1,8 @@
1
1
  # flake8: noqa
2
+
3
+ from .configuration import Configuration, cfg
4
+ cfg.defaults['CLI2_TRACEBACK_DISABLE'] = ''
5
+
2
6
  from .cli import (
3
7
  cmd,
4
8
  arg,
@@ -11,13 +15,12 @@ from .cli import (
11
15
  Cli2Error,
12
16
  Cli2ValueError,
13
17
  )
14
- from .asyncio import async_resolve, Queue
18
+ from .asyncio import async_resolve, files_read
19
+ from .queue import Queue
15
20
  from .colors import colors as c
16
21
  from .theme import theme, t
17
-
18
- from .configuration import Configuration, cfg
19
22
  from .display import diff, diff_data, render, print, highlight, yaml_highlight
20
- from .interactive import choice, editor
23
+ from .interactive import confirm, choice, editor
21
24
  try:
22
25
  import fcntl
23
26
  except ImportError:
@@ -29,9 +32,17 @@ from .log import configure, log, parse
29
32
  from .mask import Mask
30
33
  from .notlevenshtein import closest, closest_path
31
34
  from .proc import Proc
35
+ from .find import Find
32
36
  from .table import Table
33
37
 
34
38
 
39
+ import os
40
+
41
+ if not bool(cfg['CLI2_TRACEBACK_DISABLE']):
42
+ from .traceback import enable
43
+ enable()
44
+
45
+
35
46
  def which(cmd):
36
47
  """ Wrapper around shutil.which, and also check for ~/.local/bin. """
37
48
  import shutil
@@ -39,6 +50,7 @@ def which(cmd):
39
50
  if path:
40
51
  return path
41
52
 
53
+ from pathlib import Path
42
54
  path = Path(os.getenv('HOME')) / '.local/bin' / cmd
43
55
  if path.exists():
44
56
  return str(path)
@@ -0,0 +1,65 @@
1
+ import aiofiles
2
+ import inspect
3
+ from . import display
4
+ from .queue import Queue
5
+
6
+
7
+ def async_iter(obj):
8
+ """ Check if an object is an async iterable. """
9
+ return inspect.isasyncgen(obj) or hasattr(obj, '__aiter__')
10
+
11
+
12
+ async def async_resolve(result, output=False):
13
+ """
14
+ Recursively resolve awaitables and async iterables.
15
+
16
+ :param result: The awaitable or async iterable to resolve
17
+ :param output: If True, print results as they are resolved. If False,
18
+ collect results.
19
+
20
+ :return: The resolved value(s). If output is True, returns None. If output
21
+ is False, returns a list of resolved values from async iterables.
22
+ """
23
+ while inspect.iscoroutine(result):
24
+ result = await result
25
+
26
+ if async_iter(result):
27
+ results = []
28
+ async for _ in result:
29
+ if output:
30
+ if (
31
+ not inspect.iscoroutine(_)
32
+ and not inspect.isasyncgen(_)
33
+ ):
34
+ display.print(_)
35
+ else:
36
+ await async_resolve(_, output=output)
37
+ else:
38
+ results.append(await async_resolve(_))
39
+ return None if output else results
40
+ return result
41
+
42
+
43
+ async def files_read(paths, num_workers=None, mode='r', silent=False):
44
+ """
45
+ Read a list of files asynchronously with anyio.
46
+
47
+ :param paths: File paths to read.
48
+ :param num_workers: Number of workers, cpucount*2 by default.
49
+ :return: Dict of path=content
50
+ """
51
+
52
+ result = dict()
53
+
54
+ async def file_read(path):
55
+ try:
56
+ async with aiofiles.open(str(path), mode) as f:
57
+ result[path] = await f.read()
58
+ except: # noqa
59
+ if not silent:
60
+ raise
61
+
62
+ queue = Queue(num_workers=num_workers)
63
+ await queue.run(*[file_read(path) for path in paths])
64
+
65
+ return {key: result[key] for key in sorted(result)}
@@ -10,12 +10,10 @@ import textwrap
10
10
  from docstring_parser import parse
11
11
 
12
12
  from . import display
13
- from .asyncio import async_resolve
14
13
  from .colors import colors
15
-
16
-
17
- class Cli2Error(Exception):
18
- pass
14
+ from .asyncio import async_resolve
15
+ from .theme import t
16
+ from .exceptions import Cli2Error, NotFoundError
19
17
 
20
18
 
21
19
  class Cli2ValueError(Cli2Error):
@@ -115,17 +113,30 @@ class EntryPoint:
115
113
  sys.exit(self.exit_code)
116
114
 
117
115
  def print(self, *args, sep=' ', end='\n', file=None, color=None):
118
- if args and args[0].lower() in colors.__dict__ and not color:
116
+ if args and args[0].lower() in t.__dict__ and not color:
117
+ color_name = args[0]
118
+ args = args[1:]
119
+ color = getattr(t, color_name.lower())
120
+ if color_name.lower() != color_name:
121
+ color = getattr(color, 'bold')
122
+ elif args and args[0].lower() in colors.__dict__ and not color:
123
+ # backward compatibility
119
124
  color = args[0]
120
125
  args = args[1:]
121
126
  if color.lower() != color:
122
127
  color = color.lower() + 'bold'
123
128
  color = getattr(colors, color)
129
+ elif color:
130
+ color = getattr(t, color)
124
131
 
125
132
  msg = sep.join(map(str, args))
126
133
 
127
134
  if color:
128
- msg = color + msg + colors.reset
135
+ if isinstance(color, str):
136
+ # backward compatibility
137
+ msg = color + msg + colors.reset
138
+ else:
139
+ msg = color(msg)
129
140
 
130
141
  print(msg, end=end, file=file or self.outfile, flush=True)
131
142
 
@@ -164,7 +175,7 @@ class Group(EntryPoint, dict):
164
175
  self.doc = textwrap.dedent(doc).strip()
165
176
  else:
166
177
  self.doc = inspect.getdoc(self)
167
- self.color = color or colors.green
178
+ self.color = color or 'green'
168
179
  self.posix = posix
169
180
  self.parent = None
170
181
  self.cmdclass = cmdclass or Command
@@ -247,7 +258,7 @@ class Group(EntryPoint, dict):
247
258
  return ''
248
259
 
249
260
  if error:
250
- self.print('RED', 'ERROR: ' + colors.reset + error, end='\n\n')
261
+ self.print('RED', 'ERROR: ' + t.reset + error, end='\n\n')
251
262
 
252
263
  self.print('ORANGE', 'SYNOPSYS')
253
264
  chain = []
@@ -271,7 +282,7 @@ class Group(EntryPoint, dict):
271
282
  table = Table(*[
272
283
  (
273
284
  (
274
- getattr(colors, command.color, command.color),
285
+ getattr(t, command.color, command.color),
275
286
  name,
276
287
  ),
277
288
  command.help(short=True),
@@ -283,6 +294,11 @@ class Group(EntryPoint, dict):
283
294
  help.cli2 = dict(color='green')
284
295
 
285
296
  def load(self, obj):
297
+ if loader := getattr(obj, 'cli2_load', None):
298
+ loading = getattr(obj, '_cli2_loading', False)
299
+ if not loading:
300
+ obj._cli2_loading = True
301
+ return loader(self)
286
302
  if isinstance(obj, type):
287
303
  return self.load_cls(obj)
288
304
  return self.load_obj(obj)
@@ -365,8 +381,11 @@ class Command(EntryPoint, dict):
365
381
 
366
382
  def __new__(cls, target, *args, **kwargs):
367
383
  overrides = getattr(target, 'cli2', {})
368
- cls = overrides.get('cls', cls)
369
- return super().__new__(cls, *args, **kwargs)
384
+ other_cls = overrides.get('cls', cls)
385
+ if other_cls:
386
+ cls = other_cls
387
+ result = super().__new__(cls, *args, **kwargs)
388
+ return result
370
389
 
371
390
  def __init__(self, target, name=None, color=None, doc=None, posix=False,
372
391
  help_hack=True, outfile=None, log=True, overrides=None):
@@ -483,8 +502,8 @@ class Command(EntryPoint, dict):
483
502
  def cmd(cls, *args, **kwargs):
484
503
  def override(target):
485
504
  overrides = getattr(target, 'cli2', {})
486
- overrides.update(kwargs)
487
505
  overrides['cls'] = cls
506
+ overrides.update(kwargs)
488
507
  target.cli2 = overrides
489
508
 
490
509
  if len(args) == 1 and not kwargs:
@@ -516,7 +535,7 @@ class Command(EntryPoint, dict):
516
535
  )
517
536
 
518
537
  if error:
519
- self.print('RED', 'ERROR: ' + colors.reset + error, end='\n\n')
538
+ self.print('RED', 'ERROR: ' + t.reset + error, end='\n\n')
520
539
 
521
540
  self.print('ORANGE', 'SYNOPSYS')
522
541
  chain = []
@@ -640,7 +659,13 @@ class Command(EntryPoint, dict):
640
659
  finally:
641
660
  self.post_result = asyncio.run(async_resolve(self.post_call()))
642
661
 
643
- error = self.parse(*argv)
662
+ try:
663
+ error = self.parse(*argv)
664
+ except Cli2ValueError as exc:
665
+ return self.help(error=exc.args[0])
666
+ except Exception as exc:
667
+ return self.handle_exception(exc)
668
+
644
669
  if error:
645
670
  self.exit_code = 1
646
671
  return self.help(error=error)
@@ -666,11 +691,27 @@ class Command(EntryPoint, dict):
666
691
  self.post_result = self.post_call()
667
692
 
668
693
  def handle_exception(self, exc):
694
+ if isinstance(exc, NotFoundError):
695
+ print(t.red.bold(exc.title) + f': {exc.name}\n')
696
+ if exc.available:
697
+ print(t.green.bold('AVAILABLE') + ':')
698
+ display.print(exc.available)
699
+ return
700
+ elif exc.searched:
701
+ print(t.green.bold('SEARCHED') + ':')
702
+ display.print(exc.searched)
703
+ return
669
704
  raise exc
670
705
 
671
706
  async def async_call(self, *argv):
672
707
  """ Call with async stuff in single event loop """
673
- error = self.parse(*argv)
708
+ try:
709
+ error = self.parse(*argv)
710
+ except Cli2ValueError as exc:
711
+ return self.help(error=exc.args[0])
712
+ except Exception as exc:
713
+ return self.handle_exception(exc)
714
+
674
715
  if error:
675
716
  self.exit_code = 1
676
717
  return self.help(error=error)
@@ -816,6 +857,9 @@ class Command(EntryPoint, dict):
816
857
  """
817
858
  pass
818
859
 
860
+ def __repr__(self):
861
+ return f'{type(self).__name__}(name={self.name})'
862
+
819
863
 
820
864
  class Argument:
821
865
  """
@@ -923,25 +967,20 @@ class Argument:
923
967
 
924
968
  def __str__(self):
925
969
  if self.alias:
926
- out = '[' + colors.orange + self.alias[-1]
927
- out += colors.reset
970
+ out = '[' + t.orange(self.alias[-1])
928
971
 
929
972
  if self.type != bool:
930
- out += '=' + colors.green + self.param.name.upper()
931
- out += colors.reset
973
+ out += '=' + t.green(self.param.name.upper())
932
974
 
933
975
  if self.negates:
934
- out += '|' + colors.orange + self.negates[-1]
935
- out += colors.reset
976
+ out += '|' + t.orange(self.negates[-1])
936
977
 
937
978
  out += ']'
938
979
  return out
939
980
  elif self.param.kind == self.param.VAR_POSITIONAL:
940
981
  return (
941
982
  '['
942
- + colors.green
943
- + self.param.name.upper()
944
- + colors.reset
983
+ + t.green(self.param.name.upper())
945
984
  + ']...'
946
985
  )
947
986
  elif self.param.kind == self.param.VAR_KEYWORD:
@@ -949,27 +988,23 @@ class Argument:
949
988
  return (
950
989
  '['
951
990
  + prefix
952
- + colors.green
953
- + self.param.name.upper()
954
- + colors.reset
991
+ + t.green(self.param.name.upper())
955
992
  + '='
956
- + colors.green
957
- + 'VALUE'
958
- + colors.reset
993
+ + t.green('VALUE')
959
994
  + ']...'
960
995
  )
961
996
  else:
962
- return colors.green + self.param.name.upper() + colors.reset
997
+ return t.green(self.param.name.upper())
963
998
 
964
999
  def help(self):
965
1000
  """Render help for this argument."""
966
1001
  if self.alias:
967
1002
  out = ''
968
1003
  for alias in self.alias:
969
- out += colors.orange + alias + colors.reset
1004
+ out += t.orange(alias)
970
1005
  if self.type != bool:
971
1006
  out += '='
972
- out += colors.green
1007
+ out += str(t.green)
973
1008
  if self.type:
974
1009
  if isinstance(self.type, str):
975
1010
  out += self.type
@@ -977,17 +1012,16 @@ class Argument:
977
1012
  out += self.type.__name__
978
1013
  else:
979
1014
  out += self.param.name.upper()
980
- out += colors.reset
1015
+ out += str(t.reset)
981
1016
  out += ' '
982
1017
  self.cmd.print(out)
983
1018
  else:
984
- self.cmd.print(str(self) + colors.reset)
1019
+ self.cmd.print(f'{self}{t.reset}')
985
1020
 
986
1021
  if self.negates:
987
1022
  out = ''
988
1023
  for negate in self.negates:
989
- out += colors.orange + negate + colors.reset
990
- out += colors.reset
1024
+ out += t.orange(negate)
991
1025
  out += ' '
992
1026
  self.cmd.print(out)
993
1027
 
@@ -997,9 +1031,7 @@ class Argument:
997
1031
  ):
998
1032
  self.cmd.print(
999
1033
  'Default: '
1000
- + colors.blue3
1001
- + str(self.default or self.param.default)
1002
- + colors.reset
1034
+ + t.cyan(self.default or self.param.default)
1003
1035
  )
1004
1036
 
1005
1037
  if (
@@ -1009,9 +1041,7 @@ class Argument:
1009
1041
  ):
1010
1042
  self.cmd.print(
1011
1043
  'Accepted: '
1012
- + colors.blue3
1013
- + 'yes, 1, true, no, 0, false'
1014
- + colors.reset
1044
+ + t.cyan('yes, 1, true, no, 0, false')
1015
1045
  )
1016
1046
 
1017
1047
  if self.param.kind == self.param.VAR_KEYWORD:
@@ -1019,12 +1049,9 @@ class Argument:
1019
1049
  if self.cmd.posix:
1020
1050
  self.cmd.print(
1021
1051
  '--'
1022
- + colors.green
1023
- + 'something'
1024
- + colors.reset
1052
+ + t.green('something')
1025
1053
  + '='
1026
- + colors.green
1027
- + 'somearg'
1054
+ + t.green('somearg')
1028
1055
  )
1029
1056
  else:
1030
1057
  self.cmd.print('something=somearg')
@@ -31,8 +31,6 @@ import shlex
31
31
  import textwrap
32
32
  from pathlib import Path
33
33
 
34
- from .log import log
35
-
36
34
 
37
35
  class Configuration(dict):
38
36
  """
@@ -168,7 +166,12 @@ class Configuration(dict):
168
166
 
169
167
  if key in self.defaults:
170
168
  value = self.defaults[key]
171
- log.debug(f'Defaulting {key} to {value}')
169
+ try:
170
+ from .log import log
171
+ except ImportError:
172
+ pass
173
+ else:
174
+ log.debug(f'Defaulting {key} to {value}')
172
175
  return value
173
176
 
174
177
  prompt = self.questions.get(key, key)
@@ -5,6 +5,10 @@ Generic pretty display utils.
5
5
 
6
6
  By default, we will not color strings in non-interactive ttys, but you can
7
7
  force it with :envvar:`FORCE_COLOR`, ie. gitlab-ci etc
8
+
9
+ .. envvar:: CLI2_PYGMENTS_STYLE
10
+
11
+ Pygments style to use when highlighting code, monokai by default.
8
12
  """
9
13
  import difflib
10
14
  import json
@@ -15,6 +19,12 @@ import yaml
15
19
  _print = print
16
20
 
17
21
 
22
+ def color_enabled():
23
+ if 'FORCE_COLOR' in os.environ:
24
+ return bool(os.getenv('FORCE_COLOR', ''))
25
+ return sys.stdout.isatty()
26
+
27
+
18
28
  def highlight(string, lexer):
19
29
  """
20
30
  Use pygments to render a string with a lexer.
@@ -22,8 +32,7 @@ def highlight(string, lexer):
22
32
  :param string: String to render
23
33
  :param lexer: Lexer name, Yaml, Diff, etc
24
34
  """
25
- FORCE_COLOR = bool(os.getenv('FORCE_COLOR', ''))
26
- if not sys.stdout.isatty() and not FORCE_COLOR:
35
+ if not color_enabled():
27
36
  return string
28
37
 
29
38
  try:
@@ -33,9 +42,11 @@ def highlight(string, lexer):
33
42
  except ImportError:
34
43
  return string
35
44
 
36
- formatter = pygments.formatters.TerminalFormatter()
45
+ formatter = pygments.formatters.Terminal256Formatter(
46
+ style=os.getenv('CLI2_PYGMENTS_STYLE', 'monokai'),
47
+ )
37
48
  lexer = getattr(pygments.lexers, lexer + 'Lexer')()
38
- return pygments.highlight(string, lexer, formatter)
49
+ return pygments.highlight(string, lexer, formatter).rstrip()
39
50
 
40
51
 
41
52
  def yaml_dump(data):