diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 99883898b..31685219e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,10 @@ -1.5.1 ------ +In development +-------------- +* Color: Added colors module, support for colors added to cli + + +1.5.1 (In development) +---------------------- * Machines: Added ``.get()`` method for checking several commands. (`#205 `_) * Machines: ``local.cwd`` now is the current directory even if you change the directory with non-Plumbum methods. (`#207 `_) diff --git a/README.rst b/README.rst index 76ec71e7d..6ec383c79 100644 --- a/README.rst +++ b/README.rst @@ -103,7 +103,7 @@ Command nesting >>> print sudo[ifconfig["-a"]] /usr/bin/sudo /sbin/ifconfig -a >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG - lo Link encap:Local Loopback + lo Link encap:Local Loopback UP LOOPBACK RUNNING MTU:16436 Metric:1 Remote commands (over SSH) @@ -120,7 +120,7 @@ and `Paramiko `_ (a pure-Python implement >>> r_ls = remote["ls"] >>> with remote.cwd("/lib"): ... (r_ls | grep["0.so.0"])() - ... + ... u'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n' CLI applications @@ -130,22 +130,21 @@ CLI applications import logging from plumbum import cli - + class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories") - + @cli.switch("--loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) - + def main(self, *srcfiles): print "Verbose:", self.verbose - print "Include dirs:", self.include_dirs + print "Include dirs:", self.include_dirs print "Compiling:", srcfiles - - + if __name__ == "__main__": MyCompiler.run() @@ -159,6 +158,19 @@ Sample output Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') +Color controls +-------------- + +.. code-block:: python + + from plumbum import colors + with colors.red: + print("This library provides safe, flexible color access.") + print("Color", "(and styles in general)" << colors.bold, "are easy!") + print("The simple 16 colors or", '256 named colors,' << colors.orchid + colors.underline, + "or full hex colors" << colors["#129240"], 'can be used.') + print("Unsafe " + colors.bg.dark_khaki + "color access" - colors.bg + " is available too.") + .. image:: https://d2weczhvl823v0.cloudfront.net/tomerfiliba/plumbum/trend.png diff --git a/docs/_cheatsheet.rst b/docs/_cheatsheet.rst index bff9b6893..30bab5ef9 100644 --- a/docs/_cheatsheet.rst +++ b/docs/_cheatsheet.rst @@ -151,3 +151,31 @@ Sample output Compiling: ('x.cpp', 'y.cpp', 'z.cpp') See :ref:`guide-cli`. + +Color controls +-------------- + +.. code-block:: python + + from plumbum import colors + with colors.red: + print("This library provides safe, flexible color access.") + print("Color", "(and styles in general)" << colors.bold, "are easy!") + print("The simple 16 colors or", '256 named colors,' << colors.orchid + color.underline, + "or full hex colors" << colors["#129240"], 'can be used.') + print("Unsafe " + colors.bg.dark_khaki + "color access" - colors.bg + " is available too.") + +Sample output ++++++++++++++ + +.. raw:: html + +
+ +
This library provides safe color access.
+    Color (and styles in general) are easy!
+    The simple 16 colors, 256 named colors, or full hex colors can be used.
+    Unsafe color access is available too.
+
+
+ diff --git a/docs/_color_list.html b/docs/_color_list.html new file mode 100644 index 000000000..89cbd0ada --- /dev/null +++ b/docs/_color_list.html @@ -0,0 +1,301 @@ + +
+
    +
  1. #000000 Black

  2. +
  3. #C00000 Red

  4. +
  5. #00C000 Green

  6. +
  7. #C0C000 Yellow

  8. +
  9. #0000C0 Blue

  10. +
  11. #C000C0 Magenta

  12. +
  13. #00C0C0 Cyan

  14. +
  15. #C0C0C0 LightGray

  16. +
  17. #808080 DarkGray

  18. +
  19. #FF0000 LightRed

  20. +
  21. #00FF00 LightGreen

  22. +
  23. #FFFF00 LightYellow

  24. +
  25. #0000FF LightBlue

  26. +
  27. #FF00FF LightMagenta

  28. +
  29. #00FFFF LightCyan

  30. +
  31. #FFFFFF White

  32. +
  33. #000000 Grey0

  34. +
  35. #00005F NavyBlue

  36. +
  37. #000087 DarkBlue

  38. +
  39. #0000AF Blue3

  40. +
  41. #0000D7 Blue3A

  42. +
  43. #0000FF Blue1

  44. +
  45. #005F00 DarkGreen

  46. +
  47. #005F5F DeepSkyBlue4

  48. +
  49. #005F87 DeepSkyBlue4A

  50. +
  51. #005FAF DeepSkyBlue4B

  52. +
  53. #005FD7 DodgerBlue3

  54. +
  55. #005FFF DodgerBlue2

  56. +
  57. #008700 Green4

  58. +
  59. #00875F SpringGreen4

  60. +
  61. #008787 Turquoise4

  62. +
  63. #0087AF DeepSkyBlue3

  64. +
  65. #0087D7 DeepSkyBlue3A

  66. +
  67. #0087FF DodgerBlue1

  68. +
  69. #00AF00 Green3

  70. +
  71. #00AF5F SpringGreen3

  72. +
  73. #00AF87 DarkCyan

  74. +
  75. #00AFAF LightSeaGreen

  76. +
  77. #00AFD7 DeepSkyBlue2

  78. +
  79. #00AFFF DeepSkyBlue1

  80. +
  81. #00D700 Green3A

  82. +
  83. #00D75F SpringGreen3A

  84. +
  85. #00D787 SpringGreen2

  86. +
  87. #00D7AF Cyan3

  88. +
  89. #00D7D7 DarkTurquoise

  90. +
  91. #00D7FF Turquoise2

  92. +
  93. #00FF00 Green1

  94. +
  95. #00FF5F SpringGreen2A

  96. +
  97. #00FF87 SpringGreen1

  98. +
  99. #00FFAF MediumSpringGreen

  100. +
  101. #00FFD7 Cyan2

  102. +
  103. #00FFFF Cyan1

  104. +
  105. #5F0000 DarkRed

  106. +
  107. #5F005F DeepPink4

  108. +
  109. #5F0087 Purple4

  110. +
  111. #5F00AF Purple4A

  112. +
  113. #5F00D7 Purple3

  114. +
  115. #5F00FF BlueViolet

  116. +
  117. #5F5F00 Orange4

  118. +
  119. #5F5F5F Grey37

  120. +
  121. #5F5F87 MediumPurple4

  122. +
  123. #5F5FAF SlateBlue3

  124. +
  125. #5F5FD7 SlateBlue3A

  126. +
  127. #5F5FFF RoyalBlue1

  128. +
  129. #5F8700 Chartreuse4

  130. +
  131. #5F875F DarkSeaGreen4

  132. +
  133. #5F8787 PaleTurquoise4

  134. +
  135. #5F87AF SteelBlue

  136. +
  137. #5F87D7 SteelBlue3

  138. +
  139. #5F87FF CornflowerBlue

  140. +
  141. #5FAF00 Chartreuse3

  142. +
  143. #5FAF5F DarkSeaGreen4A

  144. +
  145. #5FAF87 CadetBlue

  146. +
  147. #5FAFAF CadetBlueA

  148. +
  149. #5FAFD7 SkyBlue3

  150. +
  151. #5FAFFF SteelBlue1

  152. +
  153. #5FD700 Chartreuse3A

  154. +
  155. #5FD75F PaleGreen3

  156. +
  157. #5FD787 SeaGreen3

  158. +
  159. #5FD7AF Aquamarine3

  160. +
  161. #5FD7D7 MediumTurquoise

  162. +
  163. #5FD7FF SteelBlue1A

  164. +
  165. #5FFF00 Chartreuse2A

  166. +
  167. #5FFF5F SeaGreen2

  168. +
  169. #5FFF87 SeaGreen1

  170. +
  171. #5FFFAF SeaGreen1A

  172. +
  173. #5FFFD7 Aquamarine1

  174. +
  175. #5FFFFF DarkSlateGray2

  176. +
  177. #870000 DarkRedA

  178. +
  179. #87005F DeepPink4A

  180. +
  181. #870087 DarkMagenta

  182. +
  183. #8700AF DarkMagentaA

  184. +
  185. #8700D7 DarkViolet

  186. +
  187. #8700FF Purple

  188. +
  189. #875F00 Orange4A

  190. +
  191. #875F5F LightPink4

  192. +
  193. #875F87 Plum4

  194. +
  195. #875FAF MediumPurple3

  196. +
  197. #875FD7 MediumPurple3A

  198. +
  199. #875FFF SlateBlue1

  200. +
  201. #878700 Yellow4

  202. +
  203. #87875F Wheat4

  204. +
  205. #878787 Grey53

  206. +
  207. #8787AF LightSlateGrey

  208. +
  209. #8787D7 MediumPurple

  210. +
  211. #8787FF LightSlateBlue

  212. +
  213. #87AF00 Yellow4A

  214. +
  215. #87AF5F DarkOliveGreen3

  216. +
  217. #87AF87 DarkSeaGreen

  218. +
  219. #87AFAF LightSkyBlue3

  220. +
  221. #87AFD7 LightSkyBlue3A

  222. +
  223. #87AFFF SkyBlue2

  224. +
  225. #87D700 Chartreuse2

  226. +
  227. #87D75F DarkOliveGreen3A

  228. +
  229. #87D787 PaleGreen3A

  230. +
  231. #87D7AF DarkSeaGreen3

  232. +
  233. #87D7D7 DarkSlateGray3

  234. +
  235. #87D7FF SkyBlue1

  236. +
  237. #87FF00 Chartreuse1

  238. +
  239. #87FF5F LightGreenA

  240. +
  241. #87FF87 LightGreenB

  242. +
  243. #87FFAF PaleGreen1

  244. +
  245. #87FFD7 Aquamarine1A

  246. +
  247. #87FFFF DarkSlateGray1

  248. +
  249. #AF0000 Red3

  250. +
  251. #AF005F DeepPink4B

  252. +
  253. #AF0087 MediumVioletRed

  254. +
  255. #AF00AF Magenta3

  256. +
  257. #AF00D7 DarkVioletA

  258. +
  259. #AF00FF PurpleA

  260. +
  261. #AF5F00 DarkOrange3

  262. +
  263. #AF5F5F IndianRed

  264. +
  265. #AF5F87 HotPink3

  266. +
  267. #AF5FAF MediumOrchid3

  268. +
  269. #AF5FD7 MediumOrchid

  270. +
  271. #AF5FFF MediumPurple2

  272. +
  273. #AF8700 DarkGoldenrod

  274. +
  275. #AF875F LightSalmon3

  276. +
  277. #AF8787 RosyBrown

  278. +
  279. #AF87AF Grey63

  280. +
  281. #AF87D7 MediumPurple2A

  282. +
  283. #AF87FF MediumPurple1

  284. +
  285. #AFAF00 Gold3

  286. +
  287. #AFAF5F DarkKhaki

  288. +
  289. #AFAF87 NavajoWhite3

  290. +
  291. #AFAFAF Grey69

  292. +
  293. #AFAFD7 LightSteelBlue3

  294. +
  295. #AFAFFF LightSteelBlue

  296. +
  297. #AFD700 Yellow3

  298. +
  299. #AFD75F DarkOliveGreen3B

  300. +
  301. #AFD787 DarkSeaGreen3A

  302. +
  303. #AFD7AF DarkSeaGreen2

  304. +
  305. #AFD7D7 LightCyan3

  306. +
  307. #AFD7FF LightSkyBlue1

  308. +
  309. #AFFF00 GreenYellow

  310. +
  311. #AFFF5F DarkOliveGreen2

  312. +
  313. #AFFF87 PaleGreen1A

  314. +
  315. #AFFFAF DarkSeaGreen2A

  316. +
  317. #AFFFD7 DarkSeaGreen1

  318. +
  319. #AFFFFF PaleTurquoise1

  320. +
  321. #D70000 Red3A

  322. +
  323. #D7005F DeepPink3

  324. +
  325. #D70087 DeepPink3A

  326. +
  327. #D700AF Magenta3A

  328. +
  329. #D700D7 Magenta3B

  330. +
  331. #D700FF Magenta2

  332. +
  333. #D75F00 DarkOrange3A

  334. +
  335. #D75F5F IndianRedA

  336. +
  337. #D75F87 HotPink3A

  338. +
  339. #D75FAF HotPink2

  340. +
  341. #D75FD7 Orchid

  342. +
  343. #D75FFF MediumOrchid1

  344. +
  345. #D78700 Orange3

  346. +
  347. #D7875F LightSalmon3A

  348. +
  349. #D78787 LightPink3

  350. +
  351. #D787AF Pink3

  352. +
  353. #D787D7 Plum3

  354. +
  355. #D787FF Violet

  356. +
  357. #D7AF00 Gold3A

  358. +
  359. #D7AF5F LightGoldenrod3

  360. +
  361. #D7AF87 Tan

  362. +
  363. #D7AFAF MistyRose3

  364. +
  365. #D7AFD7 Thistle3

  366. +
  367. #D7AFFF Plum2

  368. +
  369. #D7D700 Yellow3A

  370. +
  371. #D7D75F Khaki3

  372. +
  373. #D7D787 LightGoldenrod2

  374. +
  375. #D7D7AF LightYellow3

  376. +
  377. #D7D7D7 Grey84

  378. +
  379. #D7D7FF LightSteelBlue1

  380. +
  381. #D7FF00 Yellow2

  382. +
  383. #D7FF5F DarkOliveGreen1

  384. +
  385. #D7FF87 DarkOliveGreen1A

  386. +
  387. #D7FFAF DarkSeaGreen1A

  388. +
  389. #D7FFD7 Honeydew2

  390. +
  391. #D7FFFF LightCyan1

  392. +
  393. #FF0000 Red1

  394. +
  395. #FF005F DeepPink2

  396. +
  397. #FF0087 DeepPink1

  398. +
  399. #FF00AF DeepPink1A

  400. +
  401. #FF00D7 Magenta2A

  402. +
  403. #FF00FF Magenta1

  404. +
  405. #FF5F00 OrangeRed1

  406. +
  407. #FF5F5F IndianRed1

  408. +
  409. #FF5F87 IndianRed1A

  410. +
  411. #FF5FAF HotPink

  412. +
  413. #FF5FD7 HotPinkA

  414. +
  415. #FF5FFF MediumOrchid1A

  416. +
  417. #FF8700 DarkOrange

  418. +
  419. #FF875F Salmon1

  420. +
  421. #FF8787 LightCoral

  422. +
  423. #FF87AF PaleVioletRed1

  424. +
  425. #FF87D7 Orchid2

  426. +
  427. #FF87FF Orchid1

  428. +
  429. #FFAF00 Orange1

  430. +
  431. #FFAF5F SandyBrown

  432. +
  433. #FFAF87 LightSalmon1

  434. +
  435. #FFAFAF LightPink1

  436. +
  437. #FFAFD7 Pink1

  438. +
  439. #FFAFFF Plum1

  440. +
  441. #FFD700 Gold1

  442. +
  443. #FFD75F LightGoldenrod2A

  444. +
  445. #FFD787 LightGoldenrod2B

  446. +
  447. #FFD7AF NavajoWhite1

  448. +
  449. #FFD7D7 MistyRose1

  450. +
  451. #FFD7FF Thistle1

  452. +
  453. #FFFF00 Yellow1

  454. +
  455. #FFFF5F LightGoldenrod1

  456. +
  457. #FFFF87 Khaki1

  458. +
  459. #FFFFAF Wheat1

  460. +
  461. #FFFFD7 Cornsilk1

  462. +
  463. #FFFFFF Grey100

  464. +
  465. #080808 Grey3

  466. +
  467. #121212 Grey7

  468. +
  469. #1C1C1C Grey11

  470. +
  471. #262626 Grey15

  472. +
  473. #303030 Grey19

  474. +
  475. #3A3A3A Grey23

  476. +
  477. #444444 Grey27

  478. +
  479. #4E4E4E Grey30

  480. +
  481. #585858 Grey35

  482. +
  483. #626262 Grey39

  484. +
  485. #6C6C6C Grey42

  486. +
  487. #767676 Grey46

  488. +
  489. #808080 Grey50

  490. +
  491. #8A8A8A Grey54

  492. +
  493. #949494 Grey58

  494. +
  495. #9E9E9E Grey62

  496. +
  497. #A8A8A8 Grey66

  498. +
  499. #B2B2B2 Grey70

  500. +
  501. #BCBCBC Grey74

  502. +
  503. #C6C6C6 Grey78

  504. +
  505. #D0D0D0 Grey82

  506. +
  507. #DADADA Grey85

  508. +
  509. #E4E4E4 Grey89

  510. +
  511. #EEEEEE Grey93

  512. +
+
+
diff --git a/docs/api/cli.rst b/docs/api/cli.rst index b5e971c41..dac60abf4 100644 --- a/docs/api/cli.rst +++ b/docs/api/cli.rst @@ -8,3 +8,4 @@ Package plumbum.cli .. automodule:: plumbum.cli.terminal :members: + diff --git a/docs/api/colors.rst b/docs/api/colors.rst new file mode 100644 index 000000000..91662f026 --- /dev/null +++ b/docs/api/colors.rst @@ -0,0 +1,34 @@ +Package plumbum.colors +====================== + +.. automodule:: plumbum.colors + :members: + :special-members: + +plumbum.colorlib +---------------- + +.. automodule:: plumbum.colorlib + :members: + :special-members: + +plumbum.colorlib.styles +----------------------- + +.. automodule:: plumbum.colorlib.styles + :members: + :special-members: + +plumbum.colorlib.factories +-------------------------- + +.. automodule:: plumbum.colorlib.factories + :members: + :special-members: + +plumbum.colorlib.names +---------------------- + +.. automodule:: plumbum.colorlib.names + :members: + :special-members: diff --git a/docs/cli.rst b/docs/cli.rst index 07653a6a8..3dd13bfe1 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -73,6 +73,21 @@ class-level attributes, such as ``PROGNAME``, ``VERSION`` and ``DESCRIPTION``. F class MyApp(cli.Application): PROGNAME = "Foobar" VERSION = "7.3" + +Colors are supported through the class level attributes +``COLOR_PROGNAME``, +``COLOR_DISCRIPTION``, +``COLOR_VERSION``, +``COLOR_HEADING``, +``COLOR_USAGE``, +``COLOR_SUBCOMMANDS``, +``COLOR_GROUPS[]``, and +``COLOR_GROUPS_BODY[]``, +which should contain Style objects. The dictionaries support custom colors +for named groups. The default is ``colors.do_nothing``, but if you just want more +colorful defaults, subclass ``cli.ColorfulApplication``. + +.. versionadded:: 1.5 Switch Functions ---------------- @@ -444,6 +459,7 @@ Here's an example of running this application:: committing... + See Also -------- * `filecopy.py `_ example diff --git a/docs/color.rst b/docs/color.rst new file mode 100644 index 000000000..303a82264 --- /dev/null +++ b/docs/color.rst @@ -0,0 +1,302 @@ +.. _guide-color: + +Color tools +----------- + +.. versionadded:: 1.6.0 + + +The purpose of the `plumbum.colors` library is to make adding +text styles (such as color) to Python easy and safe. Color is often a great +addition to shell scripts, but not a necessity, and implementing it properly +is tricky. It is easy to end up with an unreadable color stuck on your terminal or +with random unreadable symbols around your text. With the color module, you get quick, +safe access to ANSI colors and attributes for your scripts. The module also provides an +API for creating other color schemes for other systems using escapes. + +.. note:: Enabling color + + ``ANSIStyle`` assumes that only a terminal on a posix-identity + system can display color. You can force the use of color globally by setting + ``colors.use_color=True``. + +Generating colors +================ + +Styles are accessed through the ``colors`` object, which is an instance of a StyleFactory. The ``colors`` +object is actually an imitation module that wraps ``plumbum.colorlib.ansicolors`` with module-like access. +Thus, things like from ``plumbum.colors.bg import red`` work also. The library actually lives in ``plubmumb.colorlib``. + + +Style Factory +^^^^^^^^^^^^^ + +The ``colors`` object has the following available objects: + + ``fg`` and ``bg`` + The foreground and background colors, reset to default with ``colors.fg.reset`` + or ``~colors.fg`` and likewise for ``bg``. These are ``ColorFactory`` instances. + ``bold``, ``dim``, ``underline``, ``italics``, ``reverse``, ``strikeout``, and ``hidden`` + All the `ANSI` modifiers are available, as well as their negations, such + as ``~colors.bold`` or ``colors.bold.reset``, etc. (These are generated automatically + based on the Style attached to the factory.) + ``reset`` + The global reset will restore all properties at once. + ``do_nothing`` + Does nothing at all, but otherwise acts like any ``Style`` object. It is its own inverse. Useful for ``cli`` properties. + +The ``colors`` object can be used in a with statement, which resets all styles on leaving +the statement body. Although factories do support +some of the same methods as a Style, their primary purpose is to generate Styles. The colors object has a +``use_color`` property that can be set to force the use of color. A ``stdout`` property is provided +to make changing the output of color statement easier. A ``colors.from_ansi(code)`` method allows +you to create a Style from any ansi sequence, even complex or combined ones. + +Color Factories +^^^^^^^^^^^^^^^ + +The ``colors.fg`` and ``colors.bg`` are ``ColorFactory``'s. In fact, the colors object itself acts exactly +like the ``colors.fg`` object, with the exception of the properties listed above. + +Named foreground colors are available +directly as methods. The first 16 primary colors, ``black``, ``red``, ``green``, ``yellow``, +``blue``, ``magenta``, ``cyan``, etc, as well as ``reset``, are available. All 256 color +names are available, but do not populate factory directly, so that auto-completion +gives reasonable results. You can also access colors using strings and do ``colors[string]``. +Capitalization, underscores, and spaces (for strings) will be ignored. + +You can also access colors numerically with ``colors(n)`` or ``colors[n]`` +with the extended 256 color codes. The former will default to simple versions of +colors for the first 16 values. The later notation can also be used to slice. +Full hex codes can be used, too. If no match is found, +these will be the true 24 bit color value. + +The ``fg`` and ``bg`` also can be put in with statements, and they +will restore the foreground and background color only, respectively. + +``colors.rgb(r,g,b)`` will create a color from an +input red, green, and blue values (integers from 0-255). ``colors.hex(code)`` will allow +you to input an html style hex sequence. These work on ``fg`` and ``bg`` too. The ``repr`` of +styles is smart and will show you the closest color to the one you selected if you didn't exactly +select a color through RGB. + +Style manipulations +=================== + +Safe color manipulations refer to changes that reset themselves at some point. Unsafe manipulations +must be manually reset, and can leave your terminal color in an unreadable state if you forget +to reset the color or encounter an exception. If you do get the color unset on a terminal, the +following, typed into the command line, will restore it: + +.. code:: bash + + $ python -m plumbum.colors + +This also supports command line access to unsafe color manipulations, such as + +.. code:: bash + + $ python -m plumbum.colors blue + $ python -m plumbum.colors bg red + $ python -m plumbum.colors fg 123 + $ python -m plumbum.colors bg reset + $ python -m plumbum.colors underline + +You can use any path or number available as a style. + +Unsafe Manipulation +^^^^^^^^^^^^^^^^^^^ + +Styles have two unsafe operations: Concatenation (with ``+`` and a string) and calling ``.now()`` without +arguments (directly calling a style without arguments is also a shortcut for ``.now()``). These two +operations do not restore normal color to the terminal by themselves. To protect their use, +you should always use a context manager around any unsafe operation. + +An example of the usage of unsafe ``colors`` manipulations inside a context manager:: + + from plumbum import colors + + with colors: + colors.fg.red() + print('This is in red') + colors.green() + print('This is green ' + colors.underline + 'and now also underlined!') + print('Underlined' - colors.underline + ' and not underlined but still red') + print('This is completly restored, even if an exception is thrown!') + +Output: + + .. raw:: html + +

This is in red
+ This is in green and now also underlined!
+ Underlined and not underlined but still green.
+ This is completly restored, even if an exception is thrown!

+ +We can use ``colors`` instead of ``colors.fg`` for foreground colors. If we had used ``colors.fg`` +as the context manager, then non-foreground properties, such as ``colors.underline`` or +``colors.bg.YELLOW``, would not have reset those properties. Each attribute, +as well as ``fg``, ``bg``, and ``colors`` all have inverses in the ANSI standard. They are +accessed with ``~``, ``-``, or ``.reset``, and can be used to manually make these operations +safer, but there is a better way. + +Safe Manipulation +^^^^^^^^^^^^^^^^^ + +All other operations are safe; they restore the color automatically. The first, and hopefully +already obvious one, is using a Style rather than a ``colors`` or ``colors.fg`` object in a ``with`` statement. +This will set the color (using sys.stdout by default) to that color, and restore color on leaving. + +The second method is to manually wrap a string. This can be done with ``color.wrap("string")``, +``"string" << color``, ``color >> "string"``, or ``color["string"]``. +These produce strings that can be further manipulated or printed. + +.. note:: + + ``color * "string"`` is also a valid way to wrap strings and has a well understood order of + operations by most people writing or reading code. Under some conditions, having an operator + that takes preference over concatination is prefered. However, a bug in Python 2.6 causes right + multiplication with a string, such as ``"string" * color``, to be impossible to implement. + This was fixed in all newer Pythons. If you are not planning on `supporting Python + 2.6 `_, feel + free to use this method. + +Finally, you can also print a color to stdout directly using ``color("string")`` or +``color.print("string")``. Since the first can be an unsafe operation if you forget an argument, +you may prefer the latter. This +has the same syntax as the Python 3 print function. In Python 2, if you do not have +``from __future__ import print_function`` enabled, ``color.print_("string")`` is provided as +an alternative, following the PyQT convention for method names that match reserved Python syntax. + +An example of safe manipulations:: + + colors.fg.yellow('This is yellow', end='') + print(' And this is normal again.') + with colors.red: + print('Red color!') + with colors.bold: + print("This is red and bold.") + print("Not bold, but still red.") + print("Not red color or bold.") + print("This is bold and colorful!" << (colors.magenta + colors.bold), "And this is not.") + +Output: + + .. raw:: html + +

This is yellow And this is normal again.
+ Red color!
+ This is red and bold.
+
Not bold, but still red.
+
Not red color or bold.
+ This is bold and colorful! And this is not.

+ +Style Combinations +^^^^^^^^^^^^^^^^^^ + +You can combine styles with ``+``, ``*``, ``<<``, or ``>>``, and they will create a new combined Style object. Colors will not be "summed" or otherwise combined; the rightmost color will be used (this matches the expected effect of +applying the Styles individually to the strings). However, combined Styles are intelligent and know how to reset just the properties that they contain. As you have seen in the example above, the combined style ``(colors.magenta + colors.bold)`` can be used in any way a normal Style can. + +256 Color Support +================= + +While this library supports full 24 bit colors through escape sequences, +the library has special support for the "full" 256 colorset through numbers, +names or HEX html codes. Even if you use 24 bit color, the closest name is displayed +in the ``repr``. You can access the colors as +as ``colors.fg.Light_Blue``, ``colors.fg.lightblue``, ``colors.fg[12]``, ``colors.fg('Light_Blue')``, +``colors.fg('LightBlue')``, or ``colors.fg('#0000FF')``. +You can also iterate or slice the ``colors``, ``colors.fg``, or ``colors.bg`` objects. Slicing even +intelligently downgrades to the simple version of the codes if it is within the first 16 elements. +The supported colors are: + +.. raw:: html + :file: _color_list.html + +If you want to enforce a specific represenation, you can use ``.basic`` (8 color), ``.simple`` (16 color), ``.full`` (256 color), or ``.true`` (24 bit color) on a Style, and the colors in that Style will conform to the output representation and name of the best match color. The internal RGB colors +are remembered, so this is a non-destructive operation. + +The Classes +=========== + +The library consists of three primary classes, the ``Color`` class, the ``Style`` class, and the ``StyleFactory`` class. The following +portion of this document is primarily dealing with the working of the system, and is meant to facilitate extensions or work on the system. + +The ``Color`` class provides meaning to the concept of color, and can provide a variety of representations for any color. It +can be initialised from r,g,b values, or hex codes, 256 color names, or the simple color names via classmethods. If initialized +without arguments, it is the reset color. It also takes an fg True/False argument to indicate which color it is. You probably will +not be interacting with the Color class directly, and you probably will not need to subclass it, though new extensions to the +representations it can produce are welcome. + +The ``Style`` class hold two colors and a dictionary of attributes. It is the workhorse of the system and is what is produced +by the ``colors`` factory. It holds ``Color`` as ``.color_class``, which can be overridden by subclasses (again, this usually is not needed). +To create a color representation, you need to subclass ``Style`` and give it a working ``__str__`` definition. ``ANSIStyle`` is derived +from ``Style`` in this way. + +The factories, ``ColorFactory`` and ``StyleFactory``, are factory classes that are meant to provide simple access to 1 style Style classes. To use, +you need to initialize an object of ``StyleFactory`` with your intended Style. For example, ``colors`` is created by:: + + colors = StyleFactory(ANSIStyle) + +Subclassing Style +^^^^^^^^^^^^^^^^^ + +For example, if you wanted to create an HTMLStyle and HTMLcolors, you could do:: + + class HTMLStyle(Style): + attribute_names = dict(bold='b', li='li', code='code') + end = '
\n' + + def __str__(self): + result = '' + + if self.bg and not self.bg.reset: + result += ''.format(self.bg.hex_code) + if self.fg and not self.fg.reset: + result += ''.format(self.fg.hex_code) + for attr in sorted(self.attributes): + if self.attributes[attr]: + result += '<' + self.attribute_names[attr] + '>' + + for attr in reversed(sorted(self.attributes)): + if not self.attributes[attr]: + result += '' + if self.fg and self.fg.reset: + result += '' + if self.bg and self.bg.reset: + result += '' + + return result + + htmlcolors = StyleFactory(HTMLStyle) + +This doesn't support global resets, since that's not how HTML works, but otherwise is a working implementation. This is an example of how easy it is to add support for other output formats. + +An example of usage:: + + >>> "This is colored text" << htmlcolors.bold + htmlcolors.red + 'This is colored text' + + +The above color table can be generated with:: + + for color in htmlcolors: + htmlcolors.li( + "■" << color, + color.fg.hex_code << htmlcolors.code, + color.fg.name_camelcase) + + +.. note:: + + ``HTMLStyle`` is implemented in the library, as well, with the + ``htmlcolors`` object available in ``plumbum.colorlib``. It was used + to create the colored output in this document, with small changes + because ``colors.reset`` cannot be supported with HTML. + +See Also +======== + +* `colored `_ Another library with 256 color support +* `colorama `_ A library that supports colored text on Windows, + can be combined with Plumbum.color (if you force ``use_color``, doesn't support all extended colors) diff --git a/docs/index.rst b/docs/index.rst index f7ee2858f..3f19f1da6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,7 +55,7 @@ and cross-platform**. Apart from :ref:`shell-like syntax ` and :ref:`handy shortcuts `, the library provides local and :ref:`remote ` command execution (over SSH), local and remote file-system :ref:`paths `, easy working-directory and -environment :ref:`manipulation `, and a programmatic +environment :ref:`manipulation `, quick access to ANSI :ref:`colors `, and a programmatic :ref:`guide-cli` application toolkit. Now let's see some code! News @@ -114,6 +114,7 @@ you read it in order. remote utils cli + color changelog API Reference @@ -130,6 +131,7 @@ missing from the guide, so you might want to consult with the API reference in t api/machines api/path api/fs + api/colors About ===== diff --git a/examples/color.py b/examples/color.py new file mode 100755 index 000000000..43fe4af73 --- /dev/null +++ b/examples/color.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +from __future__ import with_statement, print_function + +from plumbum import colors + +with colors.fg.red: + print('This is in red') + +print('This is completly restored, even if an exception is thrown!') + +with colors: + print('It is always a good idea to be in a context manager, to avoid being', + 'left with a colorsed terminal if there is an exception!') + print(colors.bold + "This is bold and exciting!" - colors.bold) + print(colors.bg.cyan + "This is on a cyan background." + colors.reset) + print(colors.fg[42] + "If your terminal supports 256 colorss, this is colorsful!" + colors.reset) + print() + for c in colors: + print(c + u'\u2588', end='') + colors.reset() + print() + print('Colors can be reset ' + colors.underline['Too!']) + for c in colors[:16]: + print(c["This is in colors!"]) + diff --git a/examples/fullcolor.py b/examples/fullcolor.py new file mode 100755 index 000000000..3d85fc761 --- /dev/null +++ b/examples/fullcolor.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +from __future__ import with_statement, print_function +from plumbum import colors + +with colors: + print("Do you believe in color, punk? DO YOU?") + for i in range(0,255,10): + for j in range(0,255,10): + print(u''.join(colors.rgb(i,j,k)[u'\u2588'] for k in range(0,255,10))) + diff --git a/examples/geet.py b/examples/geet.py index c6b3e9cc4..eadaba02c 100755 --- a/examples/geet.py +++ b/examples/geet.py @@ -4,71 +4,83 @@ $ python geet.py no command given - + $ python geet.py leet unknown command 'leet' - + $ python geet.py --help geet v1.7.2 The l33t version control - + Usage: geet.py [SWITCHES] [SUBCOMMAND [SWITCHES]] args... Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits - + Subcommands: commit creates a new commit in the current branch; see 'geet commit --help' for more info push pushes the current local branch to the remote one; see 'geet push --help' for more info - + $ python geet.py commit --help geet commit v1.7.2 creates a new commit in the current branch - + Usage: geet commit [SWITCHES] Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits - + Switches: -a automatically add changed files -m VALUE:str sets the commit message; required - + $ python geet.py commit -m "foo" committing... """ from plumbum import cli +# To force no color support: +# from plumbum import colors +# colors.use_color = False + +try: + import colorama + colorama.init() + from plumbum import colors + colors.use_color = True +except ImportError: + pass -class Geet(cli.Application): +class Geet(cli.ColorfulApplication): """The l33t version control""" PROGNAME = "geet" VERSION = "1.7.2" - + COLOR_PROGNAME = None + verbosity = cli.SwitchAttr("--verbosity", cli.Set("low", "high", "some-very-long-name", "to-test-wrap-around"), help = "sets the verbosity level of the geet tool. doesn't really do anything except for testing line-wrapping " "in help " * 3) @Geet.subcommand("commit") -class GeetCommit(cli.Application): +class GeetCommit(cli.ColorfulApplication): """creates a new commit in the current branch""" - + auto_add = cli.Flag("-a", help = "automatically add changed files") message = cli.SwitchAttr("-m", str, mandatory = True, help = "sets the commit message") - + def main(self): print("committing...") GeetCommit.unbind_switches("-v", "--version") @Geet.subcommand("push") -class GeetPush(cli.Application): +class GeetPush(cli.ColorfulApplication): """pushes the current local branch to the remote one""" - + tags = cli.Flag("--tags", help = "whether to push tags (default is False)") - + def main(self, remote, branch = "master"): print("pushing to %s/%s..." % (remote, branch)) diff --git a/plumbum/cli/__init__.py b/plumbum/cli/__init__.py index 8ac2b4bb5..7ac0e6e20 100644 --- a/plumbum/cli/__init__.py +++ b/plumbum/cli/__init__.py @@ -1,3 +1,3 @@ from plumbum.cli.switches import SwitchError, switch, autoswitch, SwitchAttr, Flag, CountOf from plumbum.cli.switches import Range, Set, ExistingDirectory, ExistingFile, NonexistentPath, Predicate -from plumbum.cli.application import Application +from plumbum.cli.application import Application, ColorfulApplication diff --git a/plumbum/cli/application.py b/plumbum/cli/application.py index beb01e874..cd2ac9c0d 100644 --- a/plumbum/cli/application.py +++ b/plumbum/cli/application.py @@ -5,10 +5,12 @@ import functools from plumbum.lib import six from textwrap import TextWrapper +from collections import defaultdict from plumbum.cli.terminal import get_terminal_size from plumbum.cli.switches import (SwitchError, UnknownSwitch, MissingArgument, WrongArgumentType, MissingMandatorySwitch, SwitchCombinationError, PositionalArgumentsError, switch, SubcommandError, Flag, CountOf) +from plumbum import colors class ShowHelp(SwitchError): @@ -90,6 +92,26 @@ def main(self, src, dst): * ``USAGE`` - the usage line (shown in help) + * ``COLOR_PROGNAME`` - the color to print the name in, defaults to None + + * ``COLOR_PROGNAME`` - the color to print the discription in, defaults to None + + * ``COLOR_VERSION`` - the color to print the version in, defaults to None + + * ``COLOR_HEADING`` - the color for headings, can be an attribute, defaults to None + + * ``COLOR_USAGE`` - the color for usage, defaults to None + + * ``COLOR_SUBCOMMANDS`` - the color for subcommands, defaults to None + + * ``COLOR_SWITCHES`` - the color for switches, defaults to None + + * ``COLOR_METASWITCHES`` - the color for meta switches, defaults to None + + * ``COLOR_GROUPS[]`` - Dictionary for colors for the groups, defaults to empty (no colors) + + * ``COLOR_GROUPS_BODY[]`` - Dictionary for colors for the group bodies, defaults nothing (will default to using COLOR_GROUPS instead)`` + A note on sub-commands: when an application is the root, its ``parent`` attribute is set to ``None``. When it is used as a nested-command, ``parent`` will point to be its direct ancestor. Likewise, when an application is invoked with a sub-command, its ``nested_command`` attribute @@ -101,6 +123,14 @@ def main(self, src, dst): DESCRIPTION = None VERSION = None USAGE = None + COLOR_PROGNAME = None + COLOR_DISCRIPTION = None + COLOR_VERSION = None + COLOR_HEADING = None + COLOR_USAGE = None + COLOR_SUBCOMMANDS = None + COLOR_GROUPS = dict() + COLOR_GROUPS_BODY = COLOR_GROUPS CALL_MAIN_IF_NESTED_COMMAND = True parent = None @@ -108,6 +138,18 @@ def main(self, src, dst): _unbound_switches = () def __init__(self, executable): + # Convert the colors to plumbum.colors on the instance (class remains the same) + for item in ('COLOR_PROGNAME', 'COLOR_DISCRIPTION', 'COLOR_VERSION', + 'COLOR_HEADING', 'COLOR_USAGE', 'COLOR_SUBCOMMANDS'): + setattr(self, item, colors(getattr(type(self), item))) + + self.COLOR_GROUPS = defaultdict(lambda: colors()) + self.COLOR_GROUPS_BODY = defaultdict(lambda: colors()) + for item in type(self).COLOR_GROUPS: + self.COLOR_GROUPS[item] = colors(type(self).COLOR_GROUPS[item]) + for item in type(self).COLOR_GROUPS_BODY: + self.COLOR_GROUPS_BODY[item] = colors(type(self).COLOR_GROUPS_BODY[item]) + if self.PROGNAME is None: self.PROGNAME = os.path.basename(executable) if self.DESCRIPTION is None: @@ -474,7 +516,7 @@ def help(self): # @ReservedAssignment self.version() print("") if self.DESCRIPTION: - print(self.DESCRIPTION.strip()) + print(self.COLOR_DISCRIPTION[self.DESCRIPTION.strip() + '\n']) m_args, m_varargs, _, m_defaults = inspect.getargspec(self.main) tailargs = m_args[1:] # skip self @@ -485,13 +527,14 @@ def help(self): # @ReservedAssignment tailargs.append("%s..." % (m_varargs,)) tailargs = " ".join(tailargs) - print("Usage:") - if not self.USAGE: - if self._subcommands: - self.USAGE = " %(progname)s [SWITCHES] [SUBCOMMAND [SWITCHES]] %(tailargs)s\n" - else: - self.USAGE = " %(progname)s [SWITCHES] %(tailargs)s\n" - print(self.USAGE % {"progname": self.PROGNAME, "tailargs": tailargs}) + with self.COLOR_USAGE: + print(self.COLOR_HEADING["Usage:"]) + if not self.USAGE: + if self._subcommands: + self.USAGE = " %(progname)s [SWITCHES] [SUBCOMMAND [SWITCHES]] %(tailargs)s\n" + else: + self.USAGE = " %(progname)s [SWITCHES] %(tailargs)s\n" + print(self.USAGE % {"progname": self.PROGNAME, "tailargs": tailargs}) by_groups = {} for si in self._switches_by_func.values(): @@ -502,21 +545,24 @@ def help(self): # @ReservedAssignment def switchs(by_groups, show_groups): for grp, swinfos in sorted(by_groups.items(), key = lambda item: item[0]): if show_groups: - print("%s:" % (grp,)) - - for si in sorted(swinfos, key = lambda si: si.names): - swnames = ", ".join(("-" if len(n) == 1 else "--") + n for n in si.names - if n in self._switches_by_name and self._switches_by_name[n] == si) - if si.argtype: - if isinstance(si.argtype, type): - typename = si.argtype.__name__ + with (self.COLOR_HEADING + self.COLOR_GROUPS[grp]): + print("%s:" % grp) + + # Print in body color unless empty, otherwise group color, otherwise nothing + with self.COLOR_GROUPS_BODY.get(grp, self.COLOR_GROUPS[grp]): + for si in sorted(swinfos, key = lambda si: si.names): + swnames = ", ".join(("-" if len(n) == 1 else "--") + n for n in si.names + if n in self._switches_by_name and self._switches_by_name[n] == si) + if si.argtype: + if isinstance(si.argtype, type): + typename = si.argtype.__name__ + else: + typename = str(si.argtype) + argtype = " %s:%s" % (si.argname.upper(), typename) else: - typename = str(si.argtype) - argtype = " %s:%s" % (si.argname.upper(), typename) - else: - argtype = "" - prefix = swnames + argtype - yield si, prefix + argtype = "" + prefix = swnames + argtype + yield si, prefix if show_groups: print("") @@ -547,20 +593,22 @@ def switchs(by_groups, show_groups): print(description_indent % (prefix, padding, msg)) if self._subcommands: - print("Subcommands:") + with (self.COLOR_HEADING + self.COLOR_SUBCOMMANDS): + print("Subcommands:") for name, subcls in sorted(self._subcommands.items()): - subapp = subcls.get() - doc = subapp.DESCRIPTION if subapp.DESCRIPTION else inspect.getdoc(subapp) - help = doc + "; " if doc else "" # @ReservedAssignment - help += "see '%s %s --help' for more info" % (self.PROGNAME, name) + with self.COLOR_SUBCOMMANDS: + subapp = subcls.get() + doc = subapp.DESCRIPTION if subapp.DESCRIPTION else inspect.getdoc(subapp) + help = doc + "; " if doc else "" # @ReservedAssignment + help += "see '%s %s --help' for more info" % (self.PROGNAME, name) - msg = indentation.join(wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))) + msg = indentation.join(wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))) - if len(name) + wrapper.width >= cols: - padding = indentation - else: - padding = " " * max(cols - wrapper.width - len(name) - 4, 1) - print(description_indent % (name, padding, msg)) + if len(name) + wrapper.width >= cols: + padding = indentation + else: + padding = " " * max(cols - wrapper.width - len(name) - 4, 1) + print(description_indent % (name, padding, msg)) def _get_prog_version(self): ver = None @@ -576,8 +624,22 @@ def _get_prog_version(self): def version(self): """Prints the program's version and quits""" ver = self._get_prog_version() - if sys.stdout.isatty() and os.name == "posix": - fmt = "\033[0;36m%s\033[0m %s" - else: - fmt = "%s %s" - print (fmt % (self.PROGNAME, ver if ver is not None else "(version not set)")) + ver_name = self.COLOR_VERSION[ver if ver is not None else "(version not set)"] + program_name = self.COLOR_PROGNAME[self.PROGNAME] + print('%s %s' % (program_name, ver_name)) + + + +class ColorfulApplication(Application): + """Application with more colorful defaults for easy color output.""" + COLOR_PROGNAME = colors.cyan + colors.bold + COLOR_VERSION = colors.cyan + COLOR_DISCRIPTION = colors.green + COLOR_HEADING = colors.bold + COLOR_USAGE = colors.red + COLOR_SUBCOMMANDS = colors.yellow + COLOR_GROUPS = {'Switches':colors.blue, + 'Meta-switches':colors.magenta, + 'Hidden-switches':colors.cyan} + COLOR_GROUPS_BODY = COLOR_GROUPS + diff --git a/plumbum/colorlib/__init__.py b/plumbum/colorlib/__init__.py new file mode 100644 index 000000000..9fc95a6d1 --- /dev/null +++ b/plumbum/colorlib/__init__.py @@ -0,0 +1,48 @@ +"""\ +The ``ansicolor`` object provides ``bg`` and ``fg`` to access colors, +and attributes like bold and +underlined text. It also provides ``reset`` to recover the normal font. +""" + +from plumbum.colorlib.factories import StyleFactory +from plumbum.colorlib.styles import Style, ANSIStyle, HTMLStyle, ColorNotFound + +ansicolors = StyleFactory(ANSIStyle) +htmlcolors = StyleFactory(HTMLStyle) + +def load_ipython_extension(ipython): + try: + from plumbum.colorlib._ipython_ext import OutputMagics + except ImportError: + print("IPython required for the IPython extension to be loaded.") + raise + + ipython.push({"colors":htmlcolors}) + ipython.register_magics(OutputMagics) + +def main(): + """Color changing script entry. Call using + python -m plumbum.colors, will reset if no arguements given.""" + import sys + color = ' '.join(sys.argv[1:]) if len(sys.argv) > 1 else '' + ansicolors.use_color=True + get_colors_from_string(color)() + +def get_colors_from_string(color=''): + """ + Sets color based on string, use `.` or space for seperator, + and numbers, fg/bg, htmlcodes, etc all accepted (as strings). + """ + + names = color.replace('.', ' ').split() + prev = ansicolors + for name in names: + try: + prev = getattr(prev, name) + except AttributeError: + try: + prev = prev(int(name)) + except (ColorNotFound, ValueError): + prev = prev(name) + return prev if isinstance(prev, Style) else prev.reset + diff --git a/plumbum/colorlib/__main__.py b/plumbum/colorlib/__main__.py new file mode 100644 index 000000000..d848c59d4 --- /dev/null +++ b/plumbum/colorlib/__main__.py @@ -0,0 +1,9 @@ +""" +This is provided as a quick way to recover your terminal. Simply run +``python -m plumbum.colorlib`` +to recover terminal color. +""" + +from plumbum.colorlib import main + +main() diff --git a/plumbum/colorlib/_ipython_ext.py b/plumbum/colorlib/_ipython_ext.py new file mode 100644 index 000000000..dd33149c7 --- /dev/null +++ b/plumbum/colorlib/_ipython_ext.py @@ -0,0 +1,36 @@ +from IPython.core.magic import (Magics, magics_class, + cell_magic, needs_local_scope) +import IPython.display + +try: + from io import StringIO +except ImportError: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO +import sys + +valid_choices = [x[8:] for x in dir(IPython.display) if 'display_' == x[:8]] + +@magics_class +class OutputMagics(Magics): + + @needs_local_scope + @cell_magic + def to(self, line, cell, local_ns=None): + choice = line.strip() + assert choice in valid_choices, "Valid choices for '%%to' are: "+str(valid_choices) + display_fn = getattr(IPython.display, "display_"+choice) + + "Captures stdout and renders it in the notebook with some ." + with StringIO() as out: + old_out = sys.stdout + try: + sys.stdout = out + exec(cell, self.shell.user_ns, local_ns) + out.seek(0) + display_fn(out.getvalue(), raw=True) + finally: + sys.stdout = old_out + diff --git a/plumbum/colorlib/factories.py b/plumbum/colorlib/factories.py new file mode 100644 index 000000000..79a8e52fd --- /dev/null +++ b/plumbum/colorlib/factories.py @@ -0,0 +1,151 @@ +""" +Color-related factories. They produce Styles. + +""" + +from __future__ import print_function +import sys +from plumbum.colorlib.names import color_names +from plumbum.colorlib.styles import ColorNotFound + +__all__ = ['ColorFactory', 'StyleFactory'] + + +class ColorFactory(object): + + """This creates color names given fg = True/False. It usually will + be called as part of a StyleFactory.""" + + def __init__(self, fg, style): + self._fg = fg + self._style = style + self.reset = style.from_color(style.color_class(fg=fg)) + + # Adding the color name shortcuts for foreground colors + for item in color_names[:16]: + setattr(self, item, style.from_color(style.color_class.from_simple(item, fg=fg))) + + + def __getattr__(self, item): + """Full color names work, but do not populate __dir__.""" + try: + return self._style.from_color(self._style.color_class(item, fg=self._fg)) + except ColorNotFound: + raise AttributeError(item) + + def full(self, name): + """Gets the style for a color, using standard name procedure: either full + color name, html code, or number.""" + return self._style.from_color(self._style.color_class.from_full(name, fg=self._fg)) + + def simple(self, name): + """Return the extended color scheme color for a value or name.""" + return self._style.from_color(self._style.color_class.from_simple(name, fg=self._fg)) + + def rgb(self, r, g, b): + """Return the extended color scheme color for a value.""" + return self._style.from_color(self._style.color_class(r, g, b, fg=self._fg)) + + def hex(self, hexcode): + """Return the extended color scheme color for a value.""" + return self._style.from_color(self._style.color_class.from_hex(hexcode, fg=self._fg)) + + def ansi(self, ansiseq): + """Make a style from an ansi text sequence""" + return self._style.from_ansi(ansiseq) + + def __getitem__(self, val): + """\ + Shortcut to provide way to access colors numerically or by slice. + If end <= 16, will stay to simple ANSI version.""" + if isinstance(val, slice): + (start, stop, stride) = val.indices(256) + if stop <= 16: + return [self.simple(v) for v in range(start, stop, stride)] + else: + return [self.full(v) for v in range(start, stop, stride)] + + try: + return self.full(val) + except ColorNotFound: + return self.hex(val) + + def __call__(self, val_or_r=None, g = None, b = None): + """Shortcut to provide way to access colors.""" + if val_or_r is None or val_or_r is '': + return self._style() + if isinstance(val_or_r, self._style): + return self._style(val_or_r) + if isinstance(val_or_r, str) and '\033' in val_or_r: + return self.ansi(val_or_r) + return self._style.from_color(self._style.color_class(val_or_r, g, b, fg=self._fg)) + + def __iter__(self): + """Iterates through all colors in extended colorset.""" + return (self.full(i) for i in range(256)) + + def __neg__(self): + """Allows clearing a color with -""" + return self.reset + + def __invert__(self): + """Allows clearing a color with ~""" + return self.reset + + def __rsub__(self, other): + """Makes ``- COLOR.FG`` easier""" + return other + (-self) + + def __enter__(self): + """This will reset the color on leaving the with statement.""" + return self + + def __exit__(self, type, value, traceback): + """This resets a FG/BG color or all styles, + due to different definition of RESET for the + factories.""" + + self.reset.now() + return False + + def __repr__(self): + """Simple representation of the class by name.""" + return "<{0}>".format(self.__class__.__name__) + +class StyleFactory(ColorFactory): + + """Factory for styles. Holds font styles, FG and BG objects representing colors, and + imitates the FG ColorFactory to a large degree.""" + + def __init__(self, style): + super(StyleFactory,self).__init__(True, style) + + self.fg = ColorFactory(True, style) + self.bg = ColorFactory(False, style) + + self.do_nothing = style() + self.reset = style(reset=True) + + for item in style.attribute_names: + setattr(self, item, style(attributes={item:True})) + + @property + def use_color(self): + """Shortcut for setting color usage on Style""" + return self._style.use_color + + @use_color.setter + def use_color(self, val): + self._style.use_color = val + + def from_ansi(self, ansi_sequence): + """Calling this is a shortcut for creating a style from an ANSI sequence.""" + return self._style.from_ansi(ansi_sequence) + + @property + def stdout(self): + """This is a shortcut for getting stdout from a class without an instance.""" + return self._style._stdout if self._style._stdout is not None else sys.stdout + @stdout.setter + def stdout(self, newout): + self._style._stdout = newout diff --git a/plumbum/colorlib/names.py b/plumbum/colorlib/names.py new file mode 100644 index 000000000..c9ae30d32 --- /dev/null +++ b/plumbum/colorlib/names.py @@ -0,0 +1,367 @@ +''' +Names for the standard and extended color set. +Extended set is similar to `vim wiki `_, `colored `_, etc. Colors based on `wikipedia `_. + +You can access the index of the colors with names.index(name). You can access the +rgb values with ``r=int(html[n][1:3],16)``, etc. +''' + +from __future__ import division, print_function + +color_names = '''\ +black +red +green +yellow +blue +magenta +cyan +light_gray +dark_gray +light_red +light_green +light_yellow +light_blue +light_magenta +light_cyan +white +grey_0 +navy_blue +dark_blue +blue_3 +blue_3a +blue_1 +dark_green +deep_sky_blue_4 +deep_sky_blue_4a +deep_sky_blue_4b +dodger_blue_3 +dodger_blue_2 +green_4 +spring_green_4 +turquoise_4 +deep_sky_blue_3 +deep_sky_blue_3a +dodger_blue_1 +green_3 +spring_green_3 +dark_cyan +light_sea_green +deep_sky_blue_2 +deep_sky_blue_1 +green_3a +spring_green_3a +spring_green_2 +cyan_3 +dark_turquoise +turquoise_2 +green_1 +spring_green_2a +spring_green_1 +medium_spring_green +cyan_2 +cyan_1 +dark_red +deep_pink_4 +purple_4 +purple_4a +purple_3 +blue_violet +orange_4 +grey_37 +medium_purple_4 +slate_blue_3 +slate_blue_3a +royal_blue_1 +chartreuse_4 +dark_sea_green_4 +pale_turquoise_4 +steel_blue +steel_blue_3 +cornflower_blue +chartreuse_3 +dark_sea_green_4a +cadet_blue +cadet_blue_a +sky_blue_3 +steel_blue_1 +chartreuse_3a +pale_green_3 +sea_green_3 +aquamarine_3 +medium_turquoise +steel_blue_1a +chartreuse_2a +sea_green_2 +sea_green_1 +sea_green_1a +aquamarine_1 +dark_slate_gray_2 +dark_red_a +deep_pink_4a +dark_magenta +dark_magenta_a +dark_violet +purple +orange_4a +light_pink_4 +plum_4 +medium_purple_3 +medium_purple_3a +slate_blue_1 +yellow_4 +wheat_4 +grey_53 +light_slate_grey +medium_purple +light_slate_blue +yellow_4_a +dark_olive_green_3 +dark_sea_green +light_sky_blue_3 +light_sky_blue_3a +sky_blue_2 +chartreuse_2 +dark_olive_green_3a +pale_green_3a +dark_sea_green_3 +dark_slate_gray_3 +sky_blue_1 +chartreuse_1 +light_green_a +light_green_b +pale_green_1 +aquamarine_1a +dark_slate_gray_1 +red_3 +deep_pink_4b +medium_violet_red +magenta_3 +dark_violet_a +purple_a +dark_orange_3 +indian_red +hot_pink_3 +medium_orchid_3 +medium_orchid +medium_purple_2 +dark_goldenrod +light_salmon_3 +rosy_brown +grey_63 +medium_purple_2a +medium_purple_1 +gold_3 +dark_khaki +navajo_white_3 +grey_69 +light_steel_blue_3 +light_steel_blue +yellow_3 +dark_olive_green_3b +dark_sea_green_3a +dark_sea_green_2 +light_cyan_3 +light_sky_blue_1 +green_yellow +dark_olive_green_2 +pale_green_1a +dark_sea_green_2a +dark_sea_green_1 +pale_turquoise_1 +red_3a +deep_pink_3 +deep_pink_3a +magenta_3a +magenta_3b +magenta_2 +dark_orange_3a +indian_red_a +hot_pink_3a +hot_pink_2 +orchid +medium_orchid_1 +orange_3 +light_salmon_3a +light_pink_3 +pink_3 +plum_3 +violet +gold_3a +light_goldenrod_3 +tan +misty_rose_3 +thistle_3 +plum_2 +yellow_3a +khaki_3 +light_goldenrod_2 +light_yellow_3 +grey_84 +light_steel_blue_1 +yellow_2 +dark_olive_green_1 +dark_olive_green_1a +dark_sea_green_1a +honeydew_2 +light_cyan_1 +red_1 +deep_pink_2 +deep_pink_1 +deep_pink_1a +magenta_2a +magenta_1 +orange_red_1 +indian_red_1 +indian_red_1a +hot_pink +hot_pink_a +medium_orchid_1a +dark_orange +salmon_1 +light_coral +pale_violet_red_1 +orchid_2 +orchid_1 +orange_1 +sandy_brown +light_salmon_1 +light_pink_1 +pink_1 +plum_1 +gold_1 +light_goldenrod_2a +light_goldenrod_2b +navajo_white_1 +misty_rose_1 +thistle_1 +yellow_1 +light_goldenrod_1 +khaki_1 +wheat_1 +cornsilk_1 +grey_10_0 +grey_3 +grey_7 +grey_11 +grey_15 +grey_19 +grey_23 +grey_27 +grey_30 +grey_35 +grey_39 +grey_42 +grey_46 +grey_50 +grey_54 +grey_58 +grey_62 +grey_66 +grey_70 +grey_74 +grey_78 +grey_82 +grey_85 +grey_89 +grey_93'''.split() + +_greys = (3.4, 7.4, 11, 15, 19, 23, 26.7, 30.49, 34.6, 38.6, 42.4, 46.4, 50, 54, 58, 62, 66, 69.8, 73.8, 77.7, 81.6, 85.3, 89.3, 93) +_grey_vals = [int(x/100.0*16*16) for x in _greys] + +_grey_html = ['#' + format(x,'02x')*3 for x in _grey_vals] + +_normals = [int(x,16) for x in '0 5f 87 af d7 ff'.split()] +_normal_html = ['#' + format(_normals[n//36],'02x') + format(_normals[n//6%6],'02x') + format(_normals[n%6],'02x') for n in range(16-16,232-16)] + +_base_pattern = [(n//4,n//2%2,n%2) for n in range(8)] +_base_html = (['#{2:02x}{1:02x}{0:02x}'.format(x[0]*192,x[1]*192,x[2]*192) for x in _base_pattern] + + ['#808080'] + + ['#{2:02x}{1:02x}{0:02x}'.format(x[0]*255,x[1]*255,x[2]*255) for x in _base_pattern][1:]) +color_html = _base_html + _normal_html + _grey_html + +color_codes_simple = list(range(8)) + list(range(60,68)) +"""Simple colors, remember that reset is #9, second half is non as common.""" + + +# Attributes +attributes_ansi = dict( + bold=1, + dim=2, + italics=3, + underline=4, + reverse=7, + hidden=8, + strikeout=9, + ) + +#Functions to be used for color name operations + +class FindNearest(object): + """This is a class for finding the nearest color given rgb values. + Different find methods are available.""" + def __init__(self, r, g, b): + self.r = r + self.b = b + self.g = g + + def only_basic(self): + """This will only return the first 8 colors! + Breaks the colorspace into cubes, returns color""" + midlevel = 0x40 # Since bright is not included + + # The colors are originised so that it is a + # 3D cube, black at 0,0,0, white at 1,1,1 + # Compressed to linear_integers r,g,b + # [[[0,1],[2,3]],[[4,5],[6,7]]] + # r*1 + g*2 + b*4 + return (self.r>=midlevel)*1 + (self.g>=midlevel)*2 + (self.b>=midlevel)*4 + + def all_slow(self, color_slice=slice(None, None, None)): + """This is a slow way to find the nearest color.""" + distances = [self._distance_to_color(color) for color in color_html[color_slice]] + return min(range(len(distances)), key=distances.__getitem__) + + def _distance_to_color(self, color): + """This computes the distance to a color, should be minimized.""" + rgb = (int(color[1:3],16), int(color[3:5],16), int(color[5:7],16)) + return (self.r-rgb[0])**2 + (self.g-rgb[1])**2 + (self.b-rgb[2])**2 + + def _distance_to_color_number(self, n): + color = color_html[n] + return self._distance_to_color(color) + + def only_colorblock(self): + """This finds the nearest color based on block system, only works + for 17-232 color values.""" + rint = min(range(len(_normals)), key=[abs(x-self.r) for x in _normals].__getitem__) + bint = min(range(len(_normals)), key=[abs(x-self.b) for x in _normals].__getitem__) + gint = min(range(len(_normals)), key=[abs(x-self.g) for x in _normals].__getitem__) + return (16 + 36 * rint + 6 * gint + bint) + + def only_simple(self): + """Finds the simple color-block color.""" + return self.all_slow(slice(0,16,None)) + + def only_grey(self): + """Finds the greyscale color.""" + rawval = (self.r + self.b + self.g) / 3 + n = min(range(len(_grey_vals)), key=[abs(x-rawval) for x in _grey_vals].__getitem__) + return n+232 + + def all_fast(self): + """Runs roughly 8 times faster than the slow version.""" + colors = [self.only_simple(), self.only_colorblock(), self.only_grey()] + distances = [self._distance_to_color_number(n) for n in colors] + return colors[min(range(len(distances)), key=distances.__getitem__)] + +def from_html(color): + """Convert html hex code to rgb.""" + if len(color) != 7 or color[0] != '#': + raise ValueError("Invalid length of html code") + return (int(color[1:3],16), int(color[3:5],16), int(color[5:7],16)) + +def to_html(r, g, b): + """Convert rgb to html hex code.""" + return "#{0:02x}{1:02x}{2:02x}".format(r, g, b) + diff --git a/plumbum/colorlib/styles.py b/plumbum/colorlib/styles.py new file mode 100644 index 000000000..b7cd810c8 --- /dev/null +++ b/plumbum/colorlib/styles.py @@ -0,0 +1,720 @@ +""" +This file provides two classes, `Color` and `Style`. + +``Color`` is rarely used directly, +but merely provides the workhorse for finding and manipulating colors. + +With the ``Style`` class, any color can be directly called or given to a with statement. +""" + +from __future__ import print_function +import sys +import os +import re +from copy import copy +from plumbum.colorlib.names import color_names, color_html +from plumbum.colorlib.names import color_codes_simple, from_html +from plumbum.colorlib.names import FindNearest, attributes_ansi + +__all__ = ['Color', 'Style', 'ANSIStyle', 'HTMLStyle', 'ColorNotFound', 'AttributeNotFound'] + + +_lower_camel_names = [n.replace('_', '') for n in color_names] + + +class ColorNotFound(Exception): + """Thrown when a color is not valid for a particular method.""" + pass + +class AttributeNotFound(Exception): + """Similar to color not found, only for attributes.""" + pass + +class ResetNotSupported(Exception): + """An exception indicating that Reset is not available + for this Style.""" + pass + + +class Color(object): + """\ + Loaded with ``(r, g, b, fg)`` or ``(color, fg=fg)``. The second signature is a short cut + and will try full and hex loading. + + This class stores the idea of a color, rather than a specific implementation. + It provides as many different tools for representations as possible, and can be subclassed + to add more represenations, though that should not be needed for most situations. ``.from_`` class methods provide quick ways to create colors given different representations. + You will not usually interact with this class. + + Possible colors:: + + reset = Color() # The reset color by default + background_reset = Color(fg=False) # Can be a background color + blue = Color(0,0,255) # Red, Green, Blue + green = Color.from_full("green") # Case insensitive name, from large colorset + red = Color.from_full(1) # Color number + white = Color.from_html("#FFFFFF") # HTML supported + yellow = Color.from_simple("red") # Simple colorset + + + The attributes are: + + .. data:: reset + + True it this is a reset color (following attributes don't matter if True) + + .. data:: rgb + + The red/green/blue tuple for this color + + .. data:: simple + + If true will stay to 16 color mode. + + .. data:: number + + The color number given the mode, closest to rgb + if not rgb not exact, gives position of closest name. + + .. data:: fg + + This is a foreground color if True. Background color if False. + + """ + + __slots__ = ('fg', 'isreset', 'rgb', 'number', 'representation', 'exact') + + def __init__(self, r_or_color=None, g=None, b=None, fg=True): + """This works from color values, or tries to load non-simple ones.""" + + if isinstance(r_or_color, type(self)): + for item in ('fg', 'isreset', 'rgb', 'number', 'representation', 'exact'): + setattr(self, item, getattr(r_or_color, item)) + return + + self.fg = fg + self.isreset = True # Starts as reset color + self.rgb = (0,0,0) + + self.number = None + 'Number of the original color, or closest color' + + self.representation = 3 + '0 for 8 colors, 1 for 16 colors, 2 for 256 colors, 3 for true color' + + self.exact = True + 'This is false if the named color does not match the real color' + + if r_or_color is not None and None in (g,b): + try: + self._from_simple(r_or_color) + except ColorNotFound: + try: + self._from_full(r_or_color) + except ColorNotFound: + self._from_hex(r_or_color) + + + elif None not in (r_or_color, g, b): + self.rgb = (r_or_color,g,b) + self._init_number() + elif r_or_color is None and g is None and b is None: + return + else: + raise ColorNotFound("Invalid parameters for a color!") + + def _init_number(self): + """Should always be called after filling in r, g, b, and representation. + Color will not be a reset color anymore.""" + + if self.representation == 0: + number = FindNearest(*self.rgb).only_basic() + elif self.representation == 1: + number = FindNearest(*self.rgb).only_simple() + else: + number = FindNearest(*self.rgb).all_fast() + + if self.number is None: + self.number = number + + self.isreset = False + self.exact = self.rgb == from_html(color_html[self.number]) + if not self.exact: + self.number = number + + + @classmethod + def from_simple(cls, color, fg=True): + """Creates a color from simple name or color number""" + self = cls(fg=fg) + self._from_simple(color) + return self + + def _from_simple(self, color): + try: + color = color.lower() + color = color.replace(' ','') + color = color.replace('_','') + except AttributeError: + pass + + if color == 'reset': + return + + elif color in _lower_camel_names[:16]: + self.number = _lower_camel_names.index(color) + self.rgb = from_html(color_html[self.number]) + + elif isinstance(color, int) and 0 <= color < 16: + self.number = color + self.rgb = from_html(color_html[color]) + + else: + raise ColorNotFound("Did not find color: " + repr(color)) + + self.representation = 1 + self._init_number() + + @classmethod + def from_full(cls, color, fg=True): + """Creates a color from full name or color number""" + self = cls(fg=fg) + self._from_full(color) + return self + + def _from_full(self, color): + try: + color = color.lower() + color = color.replace(' ','') + color = color.replace('_','') + except AttributeError: + pass + + if color == 'reset': + return + + elif color in _lower_camel_names: + self.number = _lower_camel_names.index(color) + self.rgb = from_html(color_html[self.number]) + + elif isinstance(color, int) and 0 <= color <= 255: + self.number = color + self.rgb = from_html(color_html[color]) + + else: + raise ColorNotFound("Did not find color: " + repr(color)) + + self.representation = 2 + self._init_number() + + @classmethod + def from_hex(cls, color, fg=True): + """Converts #123456 values to colors.""" + + self = cls(fg=fg) + self._from_hex(color) + return self + + def _from_hex(self, color): + try: + self.rgb = from_html(color) + except (TypeError, ValueError): + raise ColorNotFound("Did not find htmlcode: " + repr(color)) + + self.representation = 3 + self._init_number() + + @property + def name(self): + """The (closest) name of the current color""" + if self.isreset: + return 'reset' + else: + return color_names[self.number] + + @property + def name_camelcase(self): + """The camelcase name of the color""" + return self.name.replace("_", " ").title().replace(" ","") + + def __repr__(self): + """This class has a smart representation that shows name and color (if not unique).""" + name = [' Basic:', '', ' Full:', ' True:'][self.representation] + name += '' if self.fg else ' Background' + name += ' ' + self.name_camelcase + name += '' if self.exact else ' ' + self.hex_code + return name[1:] + + def __eq__(self, other): + """Reset colors are equal, otherwise rgb have to match.""" + if self.isreset: + return other.isreset + else: + return self.rgb == other.rgb + + @property + def ansi_sequence(self): + """This is the ansi seqeunce as a string, ready to use.""" + return '\033[' + ';'.join(map(str, self.ansi_codes)) + 'm' + + @property + def ansi_codes(self): + """This is the full ANSI code, can be reset, simple, 256, or full color.""" + ansi_addition = 30 if self.fg else 40 + + if self.isreset: + return (ansi_addition+9,) + elif self.representation < 2: + return (color_codes_simple[self.number]+ansi_addition,) + elif self.representation == 2: + return (ansi_addition+8, 5, self.number) + else: + return (ansi_addition+8, 2, self.rgb[0], self.rgb[1], self.rgb[2]) + + @property + def hex_code(self): + """This is the hex code of the current color, html style notation.""" + if self.isreset: + return '#000000' + else: + return '#' + '{0[0]:02X}{0[1]:02X}{0[2]:02X}'.format(self.rgb) + + def __str__(self): + """This just prints it's simple name""" + return self.name + + def to_representation(self, val): + """Converts a color to any represntation""" + other = copy(self) + other.representation = val + other.number = None + other._init_number() + return other + + + + +class Style(object): + """This class allows the color changes to be called directly + to write them to stdout, ``[]`` calls to wrap colors (or the ``.wrap`` method) + and can be called in a with statement. + """ + + __slots__ = ('attributes','fg', 'bg', 'isreset') + + color_class = Color + """The class of color to use. Never hardcode ``Color`` call when writing a Style + method.""" + + attribute_names = None # should be a dict of valid names + _stdout = None + end = '\n' + """The endline character. Override if needed in subclasses.""" + + @property + def stdout(self): + """\ + This property will allow custom, class level control of stdout. + It will use current sys.stdout if set to None (default). + Unfortunatly, it only works on an instance.. + """ + return self.__class__._stdout if self.__class__._stdout is not None else sys.stdout + @stdout.setter + def stdout(self, newout): + self.__class__._stdout = newout + + def __init__(self, attributes=None, fgcolor=None, bgcolor=None, reset=False): + """This is usually initialized from a factory.""" + if isinstance(attributes, type(self)): + for item in ('attributes','fg', 'bg', 'isreset'): + setattr(self, item, copy(getattr(attributes, item))) + return + self.attributes = attributes if attributes is not None else dict() + self.fg = fgcolor + self.bg = bgcolor + self.isreset = reset + invalid_attributes = set(self.attributes) - set(self.attribute_names) + if len(invalid_attributes) > 0: + raise AttributeNotFound("Attribute(s) not valid: " + ", ".join(invalid_attributes)) + + @classmethod + def from_color(cls, color): + if color.fg: + self = cls(fgcolor=color) + else: + self = cls(bgcolor=color) + return self + + + def invert(self): + """This resets current color(s) and flips the value of all + attributes present""" + + other = self.__class__() + + # Opposite of reset is reset + if self.isreset: + other.isreset = True + return other + + # Flip all attributes + for attribute in self.attributes: + other.attributes[attribute] = not self.attributes[attribute] + + # Reset only if color present + if self.fg: + other.fg = self.fg.__class__() + + if self.bg: + other.bg = self.bg.__class__() + + return other + + @property + def reset(self): + """Shortcut to access reset as a property.""" + return self.invert() + + def __copy__(self): + """Copy is supported, will make dictionary and colors unique.""" + result = self.__class__() + result.isreset = self.isreset + result.fg = copy(self.fg) + result.bg = copy(self.bg) + result.attributes = copy(self.attributes) + return result + + def __neg__(self): + """This negates the effect of the current color""" + return self.invert() + + def __invert__(self): + """This allows ~color == -color.""" + return self.invert() + + def __sub__(self, other): + """Implemented to make muliple Style objects work""" + return self + (-other) + + def __rsub__(self, other): + """Implemented to make using negatives easier""" + return other + (-self) + + def __add__(self, other): + """Adding two matching Styles results in a new style with + the combination of both. Adding with a string results in + the string concatiation of a style. + + Addition is non-communitive, with the rightmost Style property + being taken if both have the same property. + (Not safe)""" + if type(self) == type(other): + result = copy(other) + + result.isreset = self.isreset or other.isreset + for attribute in self.attributes: + if attribute not in result.attributes: + result.attributes[attribute] = self.attributes[attribute] + if not result.fg: + result.fg = self.fg + if not result.bg: + result.bg = self.bg + return result + else: + return other.__class__(self) + other + + def __radd__(self, other): + """This only gets called if the string is on the left side. (Not safe)""" + return other + other.__class__(self) + + def wrap(self, wrap_this): + """Wrap a sting in this style and its inverse.""" + return self + wrap_this - self + + def __mul__(self, other): + """This class supports ``color * color2`` syntax, + and ``color * "String" syntax too.``""" + if type(self) == type(other): + return self + other + else: + return self.wrap(other) + + def __rmul__(self, other): + """This class supports ``"String:" * color`` syntax, excpet in Python 2.6 due to bug with that Python.""" + return self.wrap(other) + + def __rlshift__(self, other): + """This class supports ``"String:" << color`` syntax""" + return self.wrap(other) + + def __rand__(self, other): + """Support for "String" & color syntax""" + return self.wrap(other) + + def __and__(self, other): + """This class supports ``color & color2`` syntax. It also supports + ``"color & "String"`` syntax too. """ + return self.__mul__(other) + + def __lshift__(self, other): + """This class supports ``color << color2`` syntax. It also supports + ``"color << "String"`` syntax too. """ + return self.__mul__(other) + + def __rrshift__(self, other): + """This class supports ``"String:" >> color`` syntax""" + return self.wrap(other) + + def __rshift__(self, other): + """This class supports ``color >> "String"`` syntax. It also supports + ``"color >> color2`` syntax too. """ + return self.__mul__(other) + + + def __call__(self, *printables, **kargs): + """\ + This is a shortcut to print color immediatly to the stdout. (Not safe) + If called with an argument, will wrap that argument and print (safe)""" + + if printables or kargs: + return self.print(*printables, **kargs) + else: + self.now() + + def now(self): + '''Immediatly writes color to stdout. (Not safe)''' + self.stdout.write(str(self)) + + def print(self, *printables, **kargs): + """\ + This acts like print; will print that argument to stdout wrapped + in Style with the same syntax as the print function in 3.4.""" + + end = kargs.get('end', self.end) + sep = kargs.get('sep', ' ') + file = kargs.get('file', self.stdout) + flush = kargs.get('flush', False) + file.write(self.wrap(sep.join(map(str,printables))) + end) + if flush: + file.flush() + + + print_ = print + """Shortcut just in case user not using __future__""" + + def __getitem__(self, wrapped): + """The [] syntax is supported for wrapping""" + return self.wrap(wrapped) + + def __enter__(self): + """Context manager support""" + self.stdout.write(str(self)) + + def __exit__(self, type, value, traceback): + """Runs even if exception occured, does not catch it.""" + self.stdout.write(str(-self)) + return False + + @property + def ansi_codes(self): + """Generates the full ANSI code sequence for a Style""" + + if self.isreset: + return [0] + + codes = [] + for attribute in self.attributes: + if self.attributes[attribute]: + codes.append(attributes_ansi[attribute]) + else: + codes.append(20+attributes_ansi[attribute]) + + if self.fg: + codes.extend(self.fg.ansi_codes) + + if self.bg: + self.bg.fg = False + codes.extend(self.bg.ansi_codes) + + return codes + + @property + def ansi_sequence(self): + """This is the string ANSI sequence.""" + codes = self.ansi_codes + if codes: + return '\033[' + ';'.join(map(str, self.ansi_codes)) + 'm' + else: + return '' + + def __repr__(self): + name = self.__class__.__name__ + attributes = ', '.join(a for a in self.attributes if self.attributes[a]) + neg_attributes = ', '.join('-'+a for a in self.attributes if not self.attributes[a]) + colors = ', '.join(repr(c) for c in [self.fg, self.bg] if c) + string = '; '.join(s for s in [attributes, neg_attributes, colors] if s) + if self.isreset: + string = 'reset' + return "<{0}: {1}>".format(name, string if string else 'empty') + + def __eq__(self, other): + """Equality is true only if reset, or if attributes, fg, and bg match.""" + if type(self) == type(other): + if self.isreset: + return other.isreset + else: + return (self.attributes == other.attributes + and self.fg == other.fg + and self.bg == other.bg) + else: + return str(self) == other + + def __str__(self): + """Base Style does not implement a __str__ representation. This is the one + required method of a subclass.""" + raise NotImplemented("This is a base style, does not have an representation") + + + @classmethod + def from_ansi(cls, ansi_string): + """This generated a style from an ansi string.""" + result = cls() + reg = re.compile('\033' + r'\[([\d;]+)m') + res = reg.search(ansi_string) + for group in res.groups(): + sequence = map(int,group.split(';')) + result.add_ansi(sequence) + return result + + def add_ansi(self, sequence): + """Adds a sequence of ansi numbers to the class""" + + values = iter(sequence) + try: + while True: + value = next(values) + if value == 38 or value == 48: + fg = value == 38 + value = next(values) + if value == 5: + value = next(values) + if fg: + self.fg = self.color_class.from_full(value) + else: + self.bg = self.color_class.from_full(value, fg=False) + elif value == 2: + r = next(values) + g = next(values) + b = next(values) + if fg: + self.fg = self.color_class(r, g, b) + else: + self.bg = self.color_class(r, g, b, fg=False) + else: + raise ColorNotFound("the value 5 or 2 should follow a 38 or 48") + elif value==0: + self.isreset = True + elif value in attributes_ansi.values(): + for name in attributes_ansi: + if value == attributes_ansi[name]: + self.attributes[name] = True + elif value in (20+n for n in attributes_ansi.values()): + for name in attributes_ansi: + if value == attributes_ansi[name] + 20: + self.attributes[name] = False + elif 30 <= value <= 37: + self.fg = self.color_class.from_simple(value-30) + elif 40 <= value <= 47: + self.bg = self.color_class.from_simple(value-40, fg=False) + elif 90 <= value <= 97: + self.fg = self.color_class.from_simple(value-90+8) + elif 100 <= value <= 107: + self.bg = self.color_class.from_simple(value-100+8, fg=False) + elif value == 39: + self.fg = self.color_class() + elif value == 49: + self.bg = self.color_class(fg=False) + else: + raise ColorNotFound("The code {0} is not recognised".format(value)) + except StopIteration: + return + + def _to_representation(self, rep): + """This converts both colors to a specific representation""" + other = copy(self) + if other.fg: + other.fg = other.fg.to_representation(rep) + if other.bg: + other.bg = other.bg.to_representation(rep) + return other + + @property + def basic(self): + """The color in the 8 color representation.""" + return self._to_representation(0) + + @property + def simple(self): + """The color in the 16 color representation.""" + return self._to_representation(1) + + @property + def full(self): + """The color in the 256 color representation.""" + return self._to_representation(2) + + @property + def true(self): + """The color in the true color representation.""" + return self._to_representation(3) + +class ANSIStyle(Style): + """This is a subclass for ANSI styles. Use it to get + color on sys.stdout tty terminals on posix systems. + + Set ``use_color = True/False`` if you want to control color + for anything using this Style.""" + + __slots__ = () + use_color = sys.stdout.isatty() and os.name == "posix" + + attribute_names = attributes_ansi + + def __str__(self): + if self.use_color: + return self.ansi_sequence + else: + return '' + +class HTMLStyle(Style): + """This was meant to be a demo of subclassing Style, but + actually can be a handy way to quicky color html text.""" + + __slots__ = () + attribute_names = dict(bold='b', em='em', italics='i', li='li', underline='span style="text-decoration: underline;"', code='code', ol='ol start=0', strikeout='s') + end = '
\n' + + def __str__(self): + + if self.isreset: + raise ResetNotSupported("HTML does not support global resets!") + + result = '' + + if self.bg and not self.bg.isreset: + result += ''.format(self.bg.hex_code) + if self.fg and not self.fg.isreset: + result += ''.format(self.fg.hex_code) + for attr in sorted(self.attributes): + if self.attributes[attr]: + result += '<' + self.attribute_names[attr] + '>' + + for attr in reversed(sorted(self.attributes)): + if not self.attributes[attr]: + result += '' + if self.fg and self.fg.isreset: + result += '' + if self.bg and self.bg.isreset: + result += '' + + return result diff --git a/plumbum/colors.py b/plumbum/colors.py new file mode 100644 index 000000000..e10772231 --- /dev/null +++ b/plumbum/colors.py @@ -0,0 +1,18 @@ +""" +This module imitates a real module, providing standard syntax +like from `plumbum.colors` and from `plumbum.colors.bg` to work alongside +all the standard syntax for colors. +""" + +import sys +import os + +from plumbum.colorlib import ansicolors, main +if __name__ == '__main__': + main() + +# Oddly, the order here matters for Python2, but not Python3 +sys.modules[__name__ + '.fg'] = ansicolors.fg +sys.modules[__name__ + '.bg'] = ansicolors.bg +sys.modules[__name__] = ansicolors + diff --git a/plumbum/commands/processes.py b/plumbum/commands/processes.py index e8d092ad4..2129ddec9 100644 --- a/plumbum/commands/processes.py +++ b/plumbum/commands/processes.py @@ -101,7 +101,7 @@ def __init__(self, msg, argv): Exception.__init__(self, msg, argv) self.argv = argv -class CommandNotFound(Exception): +class CommandNotFound(AttributeError): """Raised by :func:`local.which ` and :func:`RemoteMachine.which ` when a command was not found in the system's ``PATH``""" diff --git a/setup.py b/setup.py index bad3c721d..a43ee551a 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ author_email = "tomerfiliba@gmail.com", license = "MIT", url = "http://plumbum.readthedocs.org", - packages = ["plumbum", "plumbum.cli", "plumbum.commands", "plumbum.machines", "plumbum.path", "plumbum.fs"], + packages = ["plumbum", "plumbum.cli", "plumbum.commands", "plumbum.machines", "plumbum.path", "plumbum.fs", "plumbum.colorlib"], platforms = ["POSIX", "Windows"], provides = ["plumbum"], keywords = "path, local, remote, ssh, shell, pipe, popen, process, execution", diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 000000000..7afc8ca2c --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,3 @@ +# This is for py.test -f with xdist plugin +[pytest] +looponfailroots = . ../plumbum diff --git a/tests/test_color.py b/tests/test_color.py new file mode 100644 index 000000000..d7d9340de --- /dev/null +++ b/tests/test_color.py @@ -0,0 +1,85 @@ +from __future__ import with_statement, print_function +import unittest +from plumbum.colorlib.styles import ANSIStyle, Color, AttributeNotFound, ColorNotFound +from plumbum.colorlib.names import color_html, FindNearest + + +class TestNearestColor(unittest.TestCase): + def test_exact(self): + self.assertEqual(FindNearest(0,0,0).all_fast(),0) + for n,color in enumerate(color_html): + # Ignoring duplicates + if n not in (16, 21, 46, 51, 196, 201, 226, 231, 244): + rgb = (int(color[1:3],16), int(color[3:5],16), int(color[5:7],16)) + self.assertEqual(FindNearest(*rgb).all_fast(),n) + + def test_nearby(self): + self.assertEqual(FindNearest(1,2,2).all_fast(),0) + self.assertEqual(FindNearest(7,7,9).all_fast(),232) + + def test_simplecolor(self): + self.assertEqual(FindNearest(1,2,4).only_basic(), 0) + self.assertEqual(FindNearest(0,255,0).only_basic(), 2) + self.assertEqual(FindNearest(100,100,0).only_basic(), 3) + self.assertEqual(FindNearest(140,140,140).only_basic(), 7) + + +class TestColorLoad(unittest.TestCase): + + def test_rgb(self): + blue = Color(0,0,255) # Red, Green, Blue + self.assertEqual(blue.rgb, (0,0,255)) + + def test_simple_name(self): + green = Color.from_simple('green') + self.assertEqual(green.number, 2) + + def test_different_names(self): + self.assertEqual(Color('Dark Blue'), + Color('Dark_Blue')) + self.assertEqual(Color('Dark_blue'), + Color('Dark_Blue')) + self.assertEqual(Color('DARKBLUE'), + Color('Dark_Blue')) + self.assertEqual(Color('DarkBlue'), + Color('Dark_Blue')) + self.assertEqual(Color('Dark Green'), + Color('Dark_Green')) + + def test_loading_methods(self): + self.assertEqual(Color("Yellow"), + Color.from_full("Yellow")) + self.assertNotEqual(Color.from_full("yellow").representation, + Color.from_simple("yellow").representation) + + +class TestANSIColor(unittest.TestCase): + def setUp(self): + ANSIStyle.use_color = True + + def test_ansi(self): + self.assertEqual(str(ANSIStyle(fgcolor=Color('reset'))), '\033[39m') + self.assertEqual(str(ANSIStyle(fgcolor=Color.from_full('green'))), '\033[38;5;2m') + self.assertEqual(str(ANSIStyle(fgcolor=Color.from_simple('red'))), '\033[31m') + + +class TestStyle(unittest.TestCase): + def setUp(self): + ANSIStyle.use_color = True + + def test_InvalidAttributes(self): + pass + +class TestNearestColor(unittest.TestCase): + def test_allcolors(self): + myrange = (0,1,2,5,17,39,48,73,82,140,193,210,240,244,250,254,255) + for r in myrange: + for g in myrange: + for b in myrange: + near = FindNearest(r,g,b) + self.assertEqual(near.all_slow(),near.all_fast(), 'Tested: {0}, {1}, {2}'.format(r,g,b)) + + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_factories.py b/tests/test_factories.py new file mode 100644 index 000000000..2a62b5e84 --- /dev/null +++ b/tests/test_factories.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +from __future__ import with_statement, print_function +import unittest +from plumbum import colors +from plumbum.colorlib.styles import ANSIStyle as Style, ColorNotFound +from plumbum.colorlib import htmlcolors +import sys + + +class TestImportColors(unittest.TestCase): + def testDifferentImports(self): + import plumbum.colors + from plumbum.colors import bold + from plumbum.colors.fg import red + self.assertEqual(str(red), str(colors.red)) + self.assertEqual(str(bold), str(colors.bold)) + +class TestANSIColor(unittest.TestCase): + + def setUp(self): + colors.use_color = True + + def testColorSlice(self): + vals = colors[:8] + self.assertEqual(len(vals),8) + self.assertEqual(vals[1], colors.red) + vals = colors[40:50] + self.assertEqual(len(vals),10) + self.assertEqual(vals[1], colors.full(41)) + + def testLoadNumericalColor(self): + self.assertEqual(colors.full(2), colors[2]) + self.assertEqual(colors.simple(2), colors(2)) + self.assertEqual(colors(54), colors[54]) + + def testColorStrings(self): + self.assertEqual('\033[0m', colors.reset) + self.assertEqual('\033[1m', colors.bold) + self.assertEqual('\033[39m', colors.fg.reset) + + def testNegateIsReset(self): + self.assertEqual(colors.reset, -colors) + self.assertEqual(colors.fg.reset, -colors.fg) + self.assertEqual(colors.bg.reset, -colors.bg) + + def testShifts(self): + self.assertEqual("This" << colors.red, "This" >> colors.red) + self.assertEqual("This" << colors.red, "This" << colors.red) + if sys.version_info >= (2, 7): + self.assertEqual("This" << colors.red, "This" * colors.red) + self.assertEqual("This" << colors.red, colors.red << "This") + self.assertEqual("This" << colors.red, colors.red << "This") + self.assertEqual("This" << colors.red, colors.red * "This") + self.assertEqual(colors.red.wrap("This"), "This" << colors.red) + + def testFromPreviousColor(self): + self.assertEqual(colors(colors.red), colors.red) + self.assertEqual(colors(colors.bg.red), colors.bg.red) + self.assertEqual(colors(colors.bold), colors.bold) + + def testFromCode(self): + self.assertEqual(colors('\033[31m'),colors.red) + + def testEmptyStyle(self): + self.assertEqual(str(colors()), '') + self.assertEqual(str(colors('')), '') + self.assertEqual(str(colors(None)), '') + + def testLoadColorByName(self): + self.assertEqual(colors['LightBlue'], colors.fg['LightBlue']) + self.assertEqual(colors.bg['light_green'], colors.bg['LightGreen']) + self.assertEqual(colors['DeepSkyBlue1'], colors['#00afff']) + self.assertEqual(colors['DeepSkyBlue1'], colors.hex('#00afff')) + + self.assertEqual(colors['DeepSkyBlue1'], colors[39]) + self.assertEqual(colors.DeepSkyBlue1, colors[39]) + self.assertEqual(colors.deepskyblue1, colors[39]) + self.assertEqual(colors.Deep_Sky_Blue1, colors[39]) + self.assertEqual(colors.RED, colors.red) + + self.assertRaises(AttributeError, lambda: colors.Notacolorsatall) + + + def testMultiColor(self): + sumcolors = colors.bold + colors.blue + self.assertEqual(colors.bold.reset + colors.fg.reset, -sumcolors) + + def testSums(self): + # Sums should not be communitave, last one is used + self.assertEqual(colors.red, colors.blue + colors.red) + self.assertEqual(colors.bg.green, colors.bg.red + colors.bg.green) + + def testRepresentations(self): + colors1 = colors.full(87) + self.assertEqual(colors1, colors.DarkSlateGray2) + self.assertEqual(colors1.basic, colors.DarkSlateGray2) + self.assertEqual(str(colors1.basic), str(colors.LightGray)) + + colors2 = colors.rgb(1,45,214) + self.assertEqual(str(colors2.full), str(colors.Blue3A)) + + + def testFromAnsi(self): + for c in colors[1:7]: + self.assertEqual(c, colors.from_ansi(str(c))) + for c in colors.bg[1:7]: + self.assertEqual(c, colors.from_ansi(str(c))) + for c in colors: + self.assertEqual(c, colors.from_ansi(str(c))) + for c in colors.bg: + self.assertEqual(c, colors.from_ansi(str(c))) + for c in colors[:16]: + self.assertEqual(c, colors.from_ansi(str(c))) + for c in colors.bg[:16]: + self.assertEqual(c, colors.from_ansi(str(c))) + for c in (colors.bold, colors.underline, colors.italics): + self.assertEqual(c, colors.from_ansi(str(c))) + + col = colors.bold + colors.fg.green + colors.bg.blue + colors.underline + self.assertEqual(col, colors.from_ansi(str(col))) + col = colors.reset + self.assertEqual(col, colors.from_ansi(str(col))) + + def testWrappedColor(self): + string = 'This is a string' + wrapped = '\033[31mThis is a string\033[39m' + self.assertEqual(colors.red.wrap(string), wrapped) + self.assertEqual(string << colors.red, wrapped) + self.assertEqual(colors.red*string, wrapped) + self.assertEqual(colors.red[string], wrapped) + + newcolors = colors.blue + colors.underline + self.assertEqual(newcolors[string], string << newcolors) + self.assertEqual(newcolors.wrap(string), string << colors.blue + colors.underline) + + def testUndoColor(self): + self.assertEqual('\033[39m', -colors.fg) + self.assertEqual('\033[39m', ~colors.fg) + self.assertEqual('\033[39m', ''-colors.fg) + self.assertEqual('\033[49m', -colors.bg) + self.assertEqual('\033[49m', ''-colors.bg) + self.assertEqual('\033[21m', -colors.bold) + self.assertEqual('\033[22m', -colors.dim) + for i in range(7): + self.assertEqual('\033[39m', -colors(i)) + self.assertEqual('\033[49m', -colors.bg(i)) + self.assertEqual('\033[39m', -colors.fg(i)) + self.assertEqual('\033[49m', -colors.bg(i)) + for i in range(256): + self.assertEqual('\033[39m', -colors.fg[i]) + self.assertEqual('\033[49m', -colors.bg[i]) + self.assertEqual('\033[0m', -colors.reset) + self.assertEqual(colors.do_nothing, -colors.do_nothing) + + self.assertEqual(colors.bold.reset, -colors.bold) + + def testLackOfColor(self): + Style.use_color = False + self.assertEqual('', colors.fg.red) + self.assertEqual('', -colors.fg) + self.assertEqual('', colors.fg['LightBlue']) + + def testFromHex(self): + self.assertRaises(ColorNotFound, lambda: colors.hex('asdf')) + self.assertRaises(ColorNotFound, lambda: colors.hex('#1234Z2')) + self.assertRaises(ColorNotFound, lambda: colors.hex(12)) + + def testDirectCall(self): + colors.blue() + + if not hasattr(sys.stdout, "getvalue"): + self.fail("Need to run in buffered mode!") + + output = sys.stdout.getvalue().strip() + self.assertEquals(output,str(colors.blue)) + + def testDirectCallArgs(self): + colors.blue("This is") + + if not hasattr(sys.stdout, "getvalue"): + self.fail("Need to run in buffered mode!") + + output = sys.stdout.getvalue().strip() + self.assertEquals(output,str("This is" << colors.blue)) + + def testPrint(self): + colors.yellow.print('This is printed to stdout') + + if not hasattr(sys.stdout, "getvalue"): + self.fail("Need to run in buffered mode!") + + output = sys.stdout.getvalue().strip() + self.assertEquals(output,str(colors.yellow.wrap('This is printed to stdout'))) + + +class TestHTMLColor(unittest.TestCase): + def test_html(self): + red_tagged = 'This is tagged' + self.assertEqual(htmlcolors.red["This is tagged"], red_tagged) + self.assertEqual("This is tagged" << htmlcolors.red, red_tagged) + self.assertEqual("This is tagged" * htmlcolors.red, red_tagged) + + twin_tagged = 'This is tagged' + self.assertEqual("This is tagged" << htmlcolors.red + htmlcolors.em, twin_tagged) + self.assertEqual("This is tagged" << htmlcolors.em << htmlcolors.red, twin_tagged) + self.assertEqual(htmlcolors.em * htmlcolors.red * "This is tagged", twin_tagged) + self.assertEqual(htmlcolors.red << "This should be wrapped", "This should be wrapped" << htmlcolors.red) + +if __name__ == '__main__': + unittest.main(buffer=True) diff --git a/tests/test_visual_color.py b/tests/test_visual_color.py new file mode 100644 index 000000000..ca8237b05 --- /dev/null +++ b/tests/test_visual_color.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +from __future__ import with_statement, print_function +import unittest +from plumbum import colors + +class TestVisualColor(unittest.TestCase): + + def setUp(self): + try: + import colorama + colorama.init() + self.colorama = colorama + colors.use_color = True + print() + print("Colorama initialized") + except ImportError: + self.colorama = None + + def tearDown(self): + if self.colorama: + self.colorama.deinit() + + def testVisualColors(self): + print() + for c in colors.fg[:16]: + with c: + print('Cycle color test', end=' ') + print(' - > back to normal') + with colors: + print(colors.fg.green + "Green " + + colors.bold + "Bold " + - colors.bold + "Normal") + print("Reset all") + + def testToggleColors(self): + print() + print(colors.fg.red("This is in red"), "but this is not") + print(colors.fg.green + "Hi, " + colors.bg[23] + + "This is on a BG" - colors.bg + " and this is not") + colors.yellow.print("This is printed from color.") + colors.reset() + +if __name__ == '__main__': + unittest.main()