diff options
author | Thomas Touhey <thomas@touhey.fr> | 2021-09-11 21:09:06 +0200 |
---|---|---|
committer | Thomas Touhey <thomas@touhey.fr> | 2021-09-11 21:09:06 +0200 |
commit | 5f54b6757abee47ee883d353e4aeda9c56dd6c89 (patch) | |
tree | 20bff9751f36f0bb5d4504f1961e51d033add177 | |
parent | b3edd31df8c96035d70c0216e444d6dd9951699f (diff) |
Reworking thcolor.
34 files changed, 2746 insertions, 1710 deletions
diff --git a/.python-version b/.python-version index 0b2eb36..11aaa06 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.7.2 +3.9.5 @@ -14,6 +14,8 @@ update: docs: @$(ST) build_sphinx +check: + @$(PE) flake8 checkdocs: @$(ST) checkdocs @@ -4,13 +4,18 @@ verify_ssl = true name = 'pypi' [requires] -python_version = '3.7' +python_version = '3.9' [packages] regex = '*' [dev-packages] sphinx = '*' +sphinx_rtd_theme = '*' +sphinx-autodoc-typehints = "*" +sphinx-autobuild = '*' "collective.checkdocs" = '*' pudb = '*' pytest = '*' +flake8 = '*' +flake8-tabs = '*' diff --git a/Pipfile.lock b/Pipfile.lock index 9758a34..d62723b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "0376534457dcacd720d6a912bf87bdf10739752646bf34d5c06ef3de9863f538" + "sha256": "fd1fa6f73d5accb39252360fbedf214260d693b76bfbef4b5147055b376e2624" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" + "python_version": "3.9" }, "sources": [ { @@ -18,18 +18,50 @@ "default": { "regex": { "hashes": [ - "sha256:020429dcf9b76cc7648a99c81b3a70154e45afebc81e0b85364457fe83b525e4", - "sha256:0552802b1c3f3c7e4fee8c85e904a13c48226020aa1a0593246888a1ac55aaaf", - "sha256:308965a80b92e1fec263ac1e4f1094317809a72bc4d26be2ec8a5fd026301175", - "sha256:4d627feef04eb626397aa7bdec772774f53d63a1dc7cc5ee4d1bd2786a769d19", - "sha256:93d1f9fcb1d25e0b4bd622eeba95b080262e7f8f55e5b43c76b8a5677e67334c", - "sha256:c3859bbf29b1345d694f069ddfe53d6907b0393fda5e3794c800ad02902d78e9", - "sha256:d56ce4c7b1a189094b9bee3b81c4aeb3f1ba3e375e91627ec8561b6ab483d0a8", - "sha256:ebc5ef4e10fa3312fa1967dc0a894e6bd985a046768171f042ac3974fadc9680", - "sha256:f9cd39066048066a4abe4c18fb213bc541339728005e72263f023742fb912585" + "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468", + "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354", + "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308", + "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d", + "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc", + "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8", + "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797", + "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2", + "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13", + "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d", + "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a", + "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0", + "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73", + "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1", + "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed", + "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a", + "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b", + "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f", + "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256", + "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb", + "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2", + "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983", + "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb", + "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645", + "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8", + "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a", + "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906", + "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f", + "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c", + "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892", + "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0", + "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e", + "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e", + "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed", + "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c", + "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374", + "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd", + "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791", + "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a", + "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1", + "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759" ], "index": "pypi", - "version": "==2019.4.14" + "version": "==2021.8.28" } }, "develop": { @@ -40,40 +72,36 @@ ], "version": "==0.7.12" }, - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" - ], - "version": "==1.3.0" - }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "version": "==19.1.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" }, "babel": { "hashes": [ - "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", - "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23" + "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", + "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" ], - "version": "==2.6.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.1" }, "certifi": { "hashes": [ - "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", - "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], - "version": "==2019.3.9" + "version": "==2021.5.30" }, - "chardet": { + "charset-normalizer": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", + "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" ], - "version": "==3.0.4" + "markers": "python_version >= '3'", + "version": "==2.0.4" }, "collective.checkdocs": { "hashes": [ @@ -82,216 +110,414 @@ "index": "pypi", "version": "==0.2" }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.4.4" + }, "docutils": { "hashes": [ - "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", - "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", - "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.16" + }, + "editorconfig": { + "hashes": [ + "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e", + "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1" + ], + "version": "==0.12.3" + }, + "flake8": { + "hashes": [ + "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", + "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" + ], + "index": "pypi", + "version": "==3.9.2" + }, + "flake8-tabs": { + "hashes": [ + "sha256:e36c0f2038d709961510b9a80a685b28db87df6cfb1309b111d6be41e79a669b", + "sha256:f11c4b8a1537d67014c24f5296118e54b78fc43204b740e28283fa8e165130ef" ], - "version": "==0.14" + "index": "pypi", + "version": "==2.3.2" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "version": "==2.8" + "markers": "python_version >= '3'", + "version": "==3.2" }, "imagesize": { "hashes": [ - "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", - "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" + "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", + "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.2.0" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "jedi": { + "hashes": [ + "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93", + "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707" ], - "version": "==1.1.0" + "markers": "python_version >= '3.6'", + "version": "==0.18.0" }, "jinja2": { "hashes": [ - "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", - "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" ], - "version": "==2.10.1" + "markers": "python_version >= '3.6'", + "version": "==3.0.1" }, - "markupsafe": { + "livereload": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869" ], - "version": "==1.1.1" + "version": "==2.6.3" + }, + "markupsafe": { + "hashes": [ + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" }, - "more-itertools": { + "mccabe": { "hashes": [ - "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", - "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" ], - "markers": "python_version > '2.7'", - "version": "==7.0.0" + "version": "==0.6.1" }, "packaging": { "hashes": [ - "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", - "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + ], + "markers": "python_version >= '3.6'", + "version": "==21.0" + }, + "parso": { + "hashes": [ + "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398", + "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22" ], - "version": "==19.0" + "markers": "python_version >= '3.6'", + "version": "==0.8.2" }, "pluggy": { "hashes": [ - "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", - "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "version": "==0.9.0" + "markers": "python_version >= '3.6'", + "version": "==1.0.0" }, "pudb": { "hashes": [ - "sha256:ac30cfc64580958ab7265decb4cabb9141f08781ff072e9a336d5a7942ce35a6" + "sha256:309ee82b45a0ffca0bc4c7f521fd3e357589c764f339bdf9dcabb7ad40692d6e" ], "index": "pypi", - "version": "==2019.1" + "version": "==2021.1" }, "py": { "hashes": [ - "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", - "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" ], - "version": "==1.8.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" }, - "pygments": { + "pycodestyle": { + "hashes": [ + "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", + "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.7.0" + }, + "pyflakes": { "hashes": [ - "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", - "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", + "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.1" }, + "pygments": { + "hashes": [ + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" + ], + "markers": "python_version >= '3.5'", + "version": "==2.10.0" + }, "pyparsing": { "hashes": [ - "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", - "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "version": "==2.4.0" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" }, "pytest": { "hashes": [ - "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d", - "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5" + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" ], "index": "pypi", - "version": "==4.4.1" + "version": "==6.2.5" }, "pytz": { "hashes": [ - "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", - "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" ], - "version": "==2019.1" + "version": "==2021.1" }, "requests": { "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "version": "==2.21.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.26.0" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "version": "==1.12.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" }, "snowballstemmer": { "hashes": [ - "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", - "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", + "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" ], - "version": "==1.2.1" + "version": "==2.1.0" }, "sphinx": { "hashes": [ - "sha256:423280646fb37944dd3c85c58fb92a20d745793a9f6c511f59da82fa97cd404b", - "sha256:de930f42600a4fef993587633984cc5027dedba2464bcf00ddace26b40f8d9ce" + "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", + "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" ], "index": "pypi", - "version": "==2.0.1" + "version": "==4.1.2" + }, + "sphinx-autobuild": { + "hashes": [ + "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac", + "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05" + ], + "index": "pypi", + "version": "==2021.3.14" + }, + "sphinx-autodoc-typehints": { + "hashes": [ + "sha256:193617d9dbe0847281b1399d369e74e34cd959c82e02c7efde077fca908a9f52", + "sha256:5e81776ec422dd168d688ab60f034fccfafbcd94329e9537712c93003bddc04a" + ], + "index": "pypi", + "version": "==1.12.0" + }, + "sphinx-rtd-theme": { + "hashes": [ + "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", + "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f" + ], + "index": "pypi", + "version": "==0.5.2" }, "sphinxcontrib-applehelp": { "hashes": [ - "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", - "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" + "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", + "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "version": "==1.0.1" + "markers": "python_version >= '3.5'", + "version": "==1.0.2" }, "sphinxcontrib-devhelp": { "hashes": [ - "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", - "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" + "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", + "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "version": "==1.0.1" + "markers": "python_version >= '3.5'", + "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { "hashes": [ - "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", - "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7" + "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", + "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" ], - "version": "==1.0.2" + "markers": "python_version >= '3.6'", + "version": "==2.0.0" }, "sphinxcontrib-jsmath": { "hashes": [ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { "hashes": [ - "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", - "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f" + "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", + "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "version": "==1.0.2" + "markers": "python_version >= '3.5'", + "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { "hashes": [ - "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", - "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768" - ], - "version": "==1.1.3" + "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", + "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" + ], + "markers": "python_version >= '3.5'", + "version": "==1.1.5" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "tornado": { + "hashes": [ + "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", + "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c", + "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288", + "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95", + "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558", + "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", + "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", + "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d", + "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326", + "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b", + "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", + "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c", + "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910", + "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5", + "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c", + "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", + "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675", + "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd", + "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f", + "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c", + "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", + "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6", + "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05", + "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", + "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575", + "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a", + "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37", + "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795", + "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f", + "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32", + "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", + "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01", + "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4", + "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", + "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921", + "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085", + "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df", + "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102", + "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", + "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68", + "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5" + ], + "markers": "python_version >= '3.5'", + "version": "==6.1" }, "urllib3": { "hashes": [ - "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", - "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", + "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" ], - "version": "==1.24.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.6" }, "urwid": { "hashes": [ - "sha256:644d3e3900867161a2fc9287a9762753d66bd194754679adb26aede559bcccbc" + "sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae" ], - "version": "==2.0.1" + "version": "==2.1.2" } } } diff --git a/docs/Makefile b/docs/Makefile index 28d11c9..cd5fe80 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,12 +2,14 @@ # # You can set these variables from the command line. + +PE = pipenv run SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXWATCH = sphinx-autobuild +SPHINXBUILD = $(PE) sphinx-build +SPHINXWATCH = $(PE) sphinx-autobuild SOURCEDIR = . BUILDDIR = _build -WEBROOT = thcolor.touhey.pro:thcolor_doc +WEBROOT = hercule:thcolor/docs # Put it first so that "make" without argument is like "make help". help: @@ -22,7 +24,8 @@ help: # Livehtml build. livehtml: - $(SPHINXWATCH) -b html -z ../thcolor $(SPHINXOPTS) . $(BUILDDIR)/html + $(SPHINXWATCH) -b html --watch ../thcolor --ignore "**/.*.kate-swp" \ + $(SPHINXOPTS) . $(BUILDDIR)/html .PHONY: livehtml diff --git a/docs/angles.rst b/docs/angles.rst deleted file mode 100644 index 71d52a9..0000000 --- a/docs/angles.rst +++ /dev/null @@ -1,12 +0,0 @@ -Angles -====== - -Some color representations use angles as some of their properties. Angles can -have one of the following types: - -.. autoclass:: thcolor.Angle.Type - -Angles in ``thcolor`` are instances of the following class: - -.. autoclass:: thcolor.Angle - :members: type, degrees, gradiants, radiants, turns diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..2ad7a43 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,10 @@ +API Reference +============= + +If you are looking for information on a specific function, class or method, +this part of the documentation is for you. + +.. toctree:: + + api/angles + api/colors diff --git a/docs/api/angles.rst b/docs/api/angles.rst new file mode 100644 index 0000000..609bb33 --- /dev/null +++ b/docs/api/angles.rst @@ -0,0 +1,24 @@ +Angles +====== + +.. py:module:: thcolor.angles + +Some color representations use angles as some of their properties. The base +class for angles is the following: + +.. autoclass:: Angle + :members: asdegrees, asgradians, asradians, asturns + +Subclasses are the following: + +.. autoclass:: DegreesAngle + :members: degrees + +.. autoclass:: GradiansAngle + :members: gradians + +.. autoclass:: RadiansAngle + :members: radians + +.. autoclass:: TurnsAngle + :members: turns diff --git a/docs/api/colors.rst b/docs/api/colors.rst new file mode 100644 index 0000000..937cd61 --- /dev/null +++ b/docs/api/colors.rst @@ -0,0 +1,34 @@ +Colors +====== + +.. py:module:: thcolor.colors + +The base class for colors is the following: + +.. autoclass:: Color + :members: alpha, assrgb, ashsl, ashwb, ascmyk, aslab, aslch, asxyz, + replace, darker, lighter, desaturate, saturate, + css, from_text + +Subclasses are the following: + +.. autoclass:: SRGBColor + :members: red, green, blue, alpha, frombytes, fromnetscapecolorname + +.. autoclass:: HSLColor + :members: hue, saturation, lightness, alpha + +.. autoclass:: HWBColor + :members: hue, whiteness, blackness, alpha + +.. autoclass:: CMYKColor + :members: cyan, magenta, yellow, black, alpha + +.. autoclass:: LABColor + :members: lightness, a, b, alpha + +.. autoclass:: LCHColor + :members: lightness, chroma, hue, alpha + +.. autoclass:: XYZColor + :members: x, y, z, alpha diff --git a/docs/colors.rst b/docs/colors.rst deleted file mode 100644 index 7bff8d1..0000000 --- a/docs/colors.rst +++ /dev/null @@ -1,16 +0,0 @@ -Colors -====== - -Colors can have one of the following types: - -.. autoclass:: thcolor.Color.Type - -RGB colors can have one of the following profiles: - -.. autoclass:: thcolor.Color.Profile - -Colors are represented in ``thcolor`` as instances of the following class: - -.. autoclass:: thcolor.Color - :members: type, rgb, rgba, hls, hlsa, hwb, hwba, cmyk, cmyka, lab, laba, - lch, lcha, xyz, xyza, css diff --git a/docs/conf.py b/docs/conf.py index 0627b7e..7fbab7d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,12 +27,14 @@ author = 'Thomas Touhey' # The full version, including alpha/beta/rc tags def _get_release(): - from os.path import dirname, join - from pkg_resources import find_distributions as find_dist + from os.path import dirname, join, normpath + from pkg_resources import Environment as _Environment - module_path = join(dirname(__file__), '..') - dist = next(find_dist(module_path, True)) - return dist.version + module_path = normpath(join(dirname(__file__), '..')) + env = _Environment(module_path) + env.scan() + mod = env['thcolor'][0] + return mod.version release = _get_release() @@ -42,9 +44,13 @@ release = _get_release() # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc' + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx_autodoc_typehints', ] +todo_include_todos = True + # Add any paths that contain templates here, relative to this directory. templates_path = [] diff --git a/docs/discuss.rst b/docs/discuss.rst new file mode 100644 index 0000000..d7e2055 --- /dev/null +++ b/docs/discuss.rst @@ -0,0 +1,10 @@ +Discussion topics +================= + +thcolor has a number of concepts necessary for a full understanding of its +conception and an efficient use of it. You can find these concepts and +discussion topics in the following sections. + +.. toctree:: + + discuss/color-expressions diff --git a/docs/discuss/color-expressions.rst b/docs/discuss/color-expressions.rst new file mode 100644 index 0000000..0109057 --- /dev/null +++ b/docs/discuss/color-expressions.rst @@ -0,0 +1,53 @@ +.. _expr: + +Expressions +=========== + +One of the aims of thcolor was to decode text-based expressions +representing colors with possibilities challenging and even outdo +CSS color expression possibilities. This is what the following static method +is for: + +Expression concepts +------------------- + +The goal of these expressions was to embrace and extend CSS syntax, so they +are basically either basic expressions or function calls, with the following +argument types: + + * Numbers. + * Percentages. + * Angles. + * Colors. + +These elements are separated by separators (either commas, slashes, or simple +spaces) and can be passed to functions, and the calls themselves can be passed +to other functions. A function call is made in the following fashion: + +:: + + <function name>([<number | percentage | angle | color> [<separator> …]]) + +If at least one separator (even simple spaces) are required between arguments, +extraneous separators between and after the arguments are ignored. Other than +if spaces are used as separators, spaces around the parenthesis or the +separators (and "between" the separators as spaces are recognized as +separators) are ignored. + +Here are some example calls: + +:: + + rgb(1, 2, 3) + rgb ( 1 22 //// 242 , 50.0% ,/,) + hsl (0 1 50 % / 22) + gray ( red( #123456 )/0.2/) + +Defining a reference +-------------------- + +Functions and color names are defined in a reference: + +.. todo:: + + More about this. diff --git a/docs/expressions.rst b/docs/expressions.rst deleted file mode 100644 index 6f9e645..0000000 --- a/docs/expressions.rst +++ /dev/null @@ -1,89 +0,0 @@ -.. _expr: - -Expressions -=========== - -One of the aims of the ``thcolor`` module was to decode text-based expressions -representing colors with possibilities challenging and even outdo -CSS color expression possibilities. This is what the following static method -is for: - -.. automethod:: thcolor.Color.from_text - -Expression concepts -------------------- - -The goal of these expressions was to embrace and extend CSS syntax, so they -are basically either basic expressions or function calls, with the following -argument types: - -.. autoclass:: thcolor.Reference.number - :members: - -.. autoclass:: thcolor.Reference.percentage - :members: - -.. autoclass:: thcolor.Reference.angle - :members: - -.. autoclass:: thcolor.Reference.color - :members: - -These elements are separated by separators (either commas, slashes, or simple -spaces) and can be passed to functions, and the calls themselves can be passed -to other functions. A function call is made in the following fashion: - -:: - - <function name>(<number | percentage | angle | color> [<separator> …]) - -If at least one separator (even simple spaces) are required between arguments, -extraneous separators between and after the arguments are ignored. Other than -if spaces are used as separators, spaces around the parenthesis or the -separators (and "between" the separators as spaces are recognized as -separators) are ignored. - -Here are some example calls: - -:: - - rgb(1, 2, 3) - rgb ( 1 22 //// 242 , 50.0% ,/,) - hsl (0 1 50 % / 22) - gray ( red( #123456 )/0.2/) - -In case of incorrectly formatted string, the following exception is returned: - -.. autoexception:: thcolor.ColorExpressionDecodingError - :members: - -Defining a reference --------------------- - -Functions and color names are defined in a reference: - -- color names are defined behind an overload of - :meth:`thcolor.Reference._color`. -- functions are defined as reference class methods and use `type hints - <https://www.python.org/dev/peps/pep-0484/>`_ to describe the types - they are expecting. - -The reference must be a derivative of the following class: - -.. autoclass:: thcolor.Reference - :members: _color, functions, colors, default - -Builtin references ------------------- - -The following references are defined: - -.. autoclass:: thcolor.CSS1Reference - -.. autoclass:: thcolor.CSS2Reference - -.. autoclass:: thcolor.CSS3Reference - -.. autoclass:: thcolor.CSS4Reference - -.. autoclass:: thcolor.DefaultReference diff --git a/docs/index.rst b/docs/index.rst index 33e175d..50b5c57 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,26 +5,17 @@ This module is a color management module made by `Thomas Touhey`_ (``th`` is for ``touhey``) for the `textoutpc`_ project, a BBCode to HTML translation module. It provides the following features: -- color management and conversions between formats (RGB, HSL, HWB, NCol, …). -- text-to-color using close-to-CSS format. - -To install the module, use pip: - -.. code-block:: bash - - $ pip install thcolor + * Color management and conversions between formats (RGB, HSL, HWB, NCol, …). + * Text-to-color using a format close to CSS. For more information and links, consult `the official website`_. -Table of contents ------------------ - .. toctree:: :maxdepth: 2 - angles - colors - expressions + onboarding + discuss + api .. _Thomas Touhey: https://thomas.touhey.fr/ .. _textoutpc: https://textout.touhey.pro/ diff --git a/docs/onboarding.rst b/docs/onboarding.rst new file mode 100644 index 0000000..412df44 --- /dev/null +++ b/docs/onboarding.rst @@ -0,0 +1,12 @@ +Onboarding +========== + +You're a new user trying to figure out what you can and cannot do with +thcolor, and you're willing to experiment? You're at the right place! +In this section, you will be able to install, run and start tweaking +thcolor to better suit your needs. + +.. toctree:: + + onboarding/installing + onboarding/tweaking diff --git a/docs/onboarding/installing.rst b/docs/onboarding/installing.rst new file mode 100644 index 0000000..4fee3ca --- /dev/null +++ b/docs/onboarding/installing.rst @@ -0,0 +1,35 @@ +Installing thcolor +================== + +In order to run and tweak thcolor, you must first install it; this section +will cover the need. + +Dependencies +------------ + +thcolor dependencies are pure Python dependencies, automatically installed +when using a package manager such as pip: + + * regex_, used for parsing color expressions. + +Installing thcolor using pip +---------------------------- + +To install thcolor, you can use pip with the following command: + +.. code-block:: sh + + python -m pip install thcolor + +Some notes on this command: + + * On most Linux distributions, you can directly call ``pip`` (or ``pip3`` + on those where Python 2.x is still the default); I personnally prefer + to call it through Python as a module. + * On Linux and other UNIX-like distributions where Python 2.x is still the + default, when Python 3.x is installed, you must usually call it using + ``python3`` instead of ``python``. + * On Microsoft Windows, the Python executable, when added to the PATH, + goes by the name ``py`` instead of ``python``. + +.. _regex: https://pypi.org/project/regex/ diff --git a/docs/onboarding/tweaking.rst b/docs/onboarding/tweaking.rst new file mode 100644 index 0000000..9ad9ab2 --- /dev/null +++ b/docs/onboarding/tweaking.rst @@ -0,0 +1,16 @@ +Tweaking thcolor +================ + +In order to start tweaking thcolor using Python instead of the CLI, you +can import utilities from the module. The minimal code for running the +server is the following: + +.. code-block:: python + + from thcolor import Color + + color = Color.from_text('darker(#123456 10%)') + print(color) + +For more information, please consult the discussion topics and API reference +on the current documentation. @@ -36,7 +36,7 @@ source-dir = docs universal = True [flake8] -ignore = F401, F403, E128, E131, E241, E261, E265, E271, W191 +ignore = F401, F403, E126, E127, E128, E131, E241, E261, E265, E271, W191 exclude = .git, __pycache__, build, dist, docs/conf.py, test.py, test [tool:pytest] @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -#****************************************************************************** +#************************************************************************** # Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> -# This file is part of the thcolor Python 3.x module, which is MIT-licensed. -#****************************************************************************** +# This file is part of the thcolor Python module, which is MIT-licensed. +#************************************************************************** """ Setup script for the thcolor Python package and script. """ from setuptools import setup as _setup @@ -12,7 +12,7 @@ kwargs = {} try: from sphinx.setup_command import BuildDoc as _BuildDoc kwargs['cmdclass'] = {'build_sphinx': _BuildDoc} -except: +except ImportError: pass # Actually, most of the project's data is read from the `setup.cfg` file. diff --git a/thcolor/__init__.py b/thcolor/__init__.py index 22c8a7e..116a77f 100755 --- a/thcolor/__init__.py +++ b/thcolor/__init__.py @@ -1,24 +1,17 @@ #!/usr/bin/env python3 -#****************************************************************************** -# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +#************************************************************************** +# Copyright (C) 2018-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** +#************************************************************************** """ HTML/CSS-like color parsing, mainly for the `[color]` tag. Defines the `get_color()` function which returns an rgba value. The functions in this module do not aim at being totally compliant with - the W3C standards, although it is inspired from it. -""" + the W3C standards, although it is inspired from it. """ -from ._color import Color -from ._ref import Reference -from ._angle import Angle - -from ._exc import ColorExpressionDecodingError - -__all__ = ["version", "Color", "Reference", "Angle", - "ColorExpressionDecodingError"] - -version = "0.3.1" +from .version import * +from .errors import * +from .colors import * +from .angles import * # End of file. diff --git a/thcolor/_angle.py b/thcolor/_angle.py deleted file mode 100755 index ac3198d..0000000 --- a/thcolor/_angle.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python3 -#****************************************************************************** -# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> -# This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** -""" Angle management. """ - -from math import pi as _pi -from enum import Enum as _Enum - -__all__ = ["Angle"] - -def _deg(name, value): - value = float(value) - return value # % 360.0 - -def _grad(name, value): - value = float(value) - return value # % 400.0 - -def _rad(name, value): - value = float(value) - return value # % (2 * _pi) - -def _turn(name, value): - value = float(value) - return value # % 1 - -# --- -# Angle definition. -# --- - -class Angle: - """ Class representing an angle within thcolor, used for some color - representations (most notably hue). Its constructor depends on the - first given argument, which represents the angle type as one of the - :class:`Angle.Type` constants. - - .. function:: Angle(Angle.Type.DEG, degrees) - - Create an angle with a value in degrees (canonical values are - between 0 and 360 excluded). For example, to create a 270° angle: - - .. code-block:: python - - angle = Angle(Angle.Type.DEG, 270) - - .. function:: Angle(Angle.Type.GRAD, gradiants) - - Create an angle with a value in gradiants (canonical values are - between 0 and 400 excluded). For example, to create a 565.5 - gradiants angle: - - .. code-block:: python - - angle = Angle(Angle.Type.GRAD, 565.5) - - .. function:: Angle(Angle.Type.RAD, radiants) - - Create an angle with a value in radiants (canonical values are - between 0 and 2π excluded). For example, to create a π - radiants angle: - - .. code-block:: python - - from math import pi - angle = Angle(Angle.Type.RAD, pi) - - .. function:: Angle(Angle.Type.TURN, turns) - - Create an angle with a value in turns (canonical values are - between 0 and 1 excluded). For example, to create a 3.5 turns - angle: - - .. code-block:: python - - angle = Angle(Angle.Type.TURN, 3.5) """ - - # Properties to work with: - # - # `_type`: the type as one of the `Angle.Type` constants. - # `_value`: the angle value. - - class Type(_Enum): - """ Class representing the type of an angle, its unit really. - The following types are available: - - .. data:: INVALID - - An invalid angle, for internal processing. - - .. data:: DEG - - An angle in degrees. A full circle is represented by - 360 degrees. - - .. data:: GRAD - - An angle in gradiants. A full circle is represented by - 400 gradiants. - - .. data:: RAD - - An angle in radiants. A full circle is represented by - 2π radiants. - - .. data:: TURN - - An angle in turns. A full circle is represented by 1 turn. """ - - INVALID = 0 - DEG = 1 - GRAD = 2 - RAD = 3 - TURN = 4 - - def __init__(self, *args, **kwargs): - self._type = Angle.Type.INVALID - self.set(*args, **kwargs) - - def __eq__(self, other): - if not isinstance(other, Angle): - return super().__eq__(other) - - if self._type == Angle.Type.INVALID: - return other.type == Angle.Type.INVALID - elif self._type == Angle.Type.DEG: - return other.degrees == self._value - elif self._type == Angle.Type.GRAD: - return other.gradiants == self._value - elif self._type == Angle.Type.RAD: - return other.radiants == self._value - elif self._type == Angle.Type.TURN: - return other.turns == self._value - - return False - - def __repr__(self): - args = (('type', f'{self.__class__.__name__}.{str(self._type)}'),) - if self._type in (Angle.Type.DEG, Angle.Type.GRAD, Angle.Type.RAD, - Angle.Type.TURN): - args += (('value', repr(self._value)),) - - argtext = ', '.join(f'{key} = {value}' for key, value in args) - return f"{self.__class__.__name__}({argtext})" - - # --- - # Management methods. - # --- - - def set(self, *args, **kwargs): - """ Set the angle. """ - - args = list(args) - - def _decode_varargs(*keys): - # Check for each key. - - results = () - - for names, convert_func, *value in keys: - for name in names: - if name in kwargs: - if args: - raise TypeError(f"{self.__class__.__name__}() " \ - f"got multiple values for argument {name}") - - raw_result = kwargs.pop(name) - break - else: - name = names[0] - if args: - raw_result = args.pop(0) - elif value: - raw_result = value[0] if len(value) == 1 else value - else: - raise TypeError(f"{self.__class__.__name__}() " \ - "missing a required positional argument: " \ - f"{name}") - - result = convert_func(name, raw_result) - results += (result,) - - # Check for keyword arguments for which keys are not in the set. - - if kwargs: - raise TypeError(f"{next(iter(kwargs.keys()))} is an invalid " \ - f"keyword argument for type {type}") - - return results - - # --- - # Main function body. - # --- - - # Check for the type. - - if args: - try: - type = kwargs.pop('type') - except: - type = args.pop(0) - else: - if isinstance(args[0], Color.Type): - raise TypeError(f"{self.__class__.__name__}() got " \ - "multiple values for argument 'type'") - else: - try: - type = kwargs.pop('type') - except: - type = self._type - if type == Angle.Type.INVALID: - raise TypeError(f"{self.__class__.__name__}() missing " \ - "required argument: 'type'") - - try: - type = Angle.Type(type) - except: - type = Angle.Type.DEG - - # Initialize the properties. - - if type == Angle.Type.DEG: - self._value, = _decode_varargs(\ - (('value', 'angle', 'degrees'), _deg)) - elif type == Angle.Type.GRAD: - self._value, = _decode_varargs(\ - (('value', 'angle', 'gradiants'), _grad)) - elif type == Angle.Type.RAD: - self._value, = _decode_varargs(\ - (('value', 'angle', 'radiants'), _rad)) - elif type == Angle.Type.TURN: - self._value, = _decode_varargs(\ - (('value', 'angle', 'turns'), _turn)) - else: - raise ValueError(f"invalid color type: {type}") - - # Once the arguments have been tested to be valid, we can set the - # type. - - self._type = type - - # --- - # Properties. - # --- - - @property - def type(self): - """ The read-only angle type as one of the :class:`Angle.Type` - constants. """ - - return self._type - - @property - def degrees(self): - """ The read-only angle value in degrees. If the angle isn't in degrees - already, it will be converted automatically. """ - - if self._type == Angle.Type.DEG: - return self._value - return self.turns * 360 - - @property - def gradiants(self): - """ The read-only angle value in gradiants. If the angle isn't in - gradiants already, it will be converted automatically. """ - - if self._type == Angle.Type.GRAD: - return self._value - return self.turns * 400 - - @property - def radiants(self): - """ The read-only angle value in radiants. If the angle isn't in - radiants already, it will be converted automatically. """ - - if self._type == Angle.Type.RAD: - return self._value - return self.turns * (2 * _pi) - - @property - def turns(self): - """ The read-only angle value in turns. If the angle isn't in - turns already, it will be converted automatically. """ - - if self._type == Angle.Type.DEG: - return self._value / 360 - elif self._type == Angle.Type.GRAD: - return self._value / 400 - elif self._type == Angle.Type.RAD: - return self._value / (2 * _pi) - elif self._type == Angle.Type.TURN: - return self._value - -# End of file. diff --git a/thcolor/_color.py b/thcolor/_color.py index 1bc8f6e..c877197 100755 --- a/thcolor/_color.py +++ b/thcolor/_color.py @@ -1,14 +1,26 @@ #!/usr/bin/env python3 -#****************************************************************************** +#************************************************************************** # Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** +#************************************************************************** """ HTML/CSS like color parsing, mainly for the `[color]` tag. Defines the `get_color()` function which returns an rgba value. """ +from copy import copy as _copy from enum import Enum as _Enum +from collections.abc import Sequence as _Sequence from warnings import warn as _warn +from .angles import Angle as _Angle +from .errors import ( + ColorExpressionDecodingError as _ColorExpressionDecodingError, + NotEnoughArgumentsError as _NotEnoughArgumentsError, + TooManyArgumentsError as _TooManyArgumentsError, + InvalidArgumentTypeError as _InvalidArgumentTypeError, + InvalidArgumentValueError as _InvalidArgumentValueError) + +__all__ = ["Color"] + _gg_no_re = False try: @@ -18,47 +30,32 @@ except ImportError: RuntimeWarning) _gg_no_re = True -from ._ref import Reference as _Reference -from ._angle import Angle as _Angle -from ._sys import (hls_to_rgb as _hls_to_rgb, rgb_to_hls as _rgb_to_hls, - rgb_to_hwb as _rgb_to_hwb, hwb_to_rgb as _hwb_to_rgb, - rgb_to_cmyk as _rgb_to_cmyk, cmyk_to_rgb as _cmyk_to_rgb, - lab_to_rgb as _lab_to_rgb, rgb_to_lab as _rgb_to_lab, - lab_to_lch as _lab_to_lch, lch_to_lab as _lch_to_lab, - netscape_color as _netscape_color) -from ._exc import (\ - ColorExpressionDecodingError as _ColorExpressionDecodingError, - NotEnoughArgumentsError as _NotEnoughArgumentsError, - TooManyArgumentsError as _TooManyArgumentsError, - InvalidArgumentTypeError as _InvalidArgumentTypeError, - InvalidArgumentValueError as _InvalidArgumentValueError) - -__all__ = ["Color"] - # --- # Decoding utilities. # --- _color_pattern = None + def _get_color_pattern(): global _color_pattern if _color_pattern is None: if _gg_no_re: - raise ImportError("text parsing is disabled until you install " \ + raise ImportError("text parsing is disabled until you install " "the 'regex' module, e.g. via `pip install regex`.") _color_pattern = _re.compile(r""" ( ((?P<agl_val>-? ([0-9]+\.?|[0-9]*\.[0-9]+)) \s* - (?P<agl_typ>deg|grad|rad|turn)) - | ((?P<per>[0-9]+(\.[0-9]*)? | \.[0-9]+) \s* \%) - | (?P<num>[0-9]+(\.[0-9]*)? | \.[0-9]+) - | (?P<ncol>[RYGCBM] [0-9]{0,2} (\.[0-9]*)?) - | (\# (?P<hex>[0-9a-f]{3} | [0-9a-f]{4} | [0-9a-f]{6} | [0-9a-f]{8})) - | ((?P<name>[a-z]([a-z0-9\s_-]*[a-z0-9_-])?) - ( \s* \( \s* (?P<arg> (?0)? ) \s* \) )?) + (?P<agl_typ>deg|grad|rad|turn)) + | ((?P<per>[0-9]+(\.[0-9]*)? | \.[0-9]+) \s* \%) + | (?P<num>[0-9]+(\.[0-9]*)? | \.[0-9]+) + | (?P<ncol>[RYGCBM] [0-9]{0,2} (\.[0-9]*)?) + | (\# (?P<hex>[0-9a-f]{3} | [0-9a-f]{4} + | [0-9a-f]{6} | [0-9a-f]{8})) + | ((?P<name>[a-z]([a-z0-9\s_-]*[a-z0-9_-])?) + ( \s* \( \s* (?P<arg> (?0)? ) \s* \) )?) ) \s* ((?P<sep>[,/\s])+ \s* (?P<nextargs> (?0))?)? @@ -70,25 +67,28 @@ def _get_color_pattern(): # Color initialization varargs utilities. # --- + def _color_profile(name, value): try: value = Color.Profile.from_value(value) except (TypeError, ValueError): - raise ValueError(f"{name} is not a valid color profile " \ + raise ValueError(f"{name} is not a valid color profile " f"(got {repr(value)}).") return value + def _byte(name, value): try: assert value == int(value) assert 0 <= value < 256 except (AssertionError, TypeError, ValueError): - raise ValueError(f"{name} should be a byte between 0 and 255") \ - from None + raise ValueError(f"{name} should be a byte between 0 " + "and 255") from None return value + def _signed(name, value): try: value = float(value) @@ -97,6 +97,7 @@ def _signed(name, value): return round(value, 4) + def _unsigned(name, value): try: value = float(value) @@ -106,41 +107,47 @@ def _unsigned(name, value): return round(value, 4) + def _unrestricted_percentage(name, value): try: value = float(value) assert 0.0 <= value except (AssertionError, TypeError, ValueError): - raise ValueError(f"{name} should be a proportion starting from 0") \ - from None + raise ValueError(f"{name} should be a proportion starting " + "from 0") from None return round(value, 4) + def _percentage(name, value): try: value = float(value) assert 0.0 <= value <= 1.0 except (AssertionError, TypeError, ValueError): - raise ValueError(f"{name} should be a proportion between 0 " \ + raise ValueError(f"{name} should be a proportion between 0 " "and 1.0") from None return round(value, 4) + def _hue(name, value): if isinstance(value, _Angle): pass else: try: value = _Angle(value) - except: - raise ValueError(f"{name} should be an Angle instance") + except ValueError: + raise ValueError(f"{name} should be an Angle instance") \ + from None return value + # --- # Color class definition. # --- + class Color: """ Class representing a color within thcolor. Its constructor depends on the first given argument, which represents the color type as one @@ -148,43 +155,46 @@ class Color: .. function:: Color(Color.Type.RGB, red, green, blue, alpha = 1.0) - Create a color using its red, green and blue components. Each is - expressed as a byte value, from 0 (dark) to 255 (light). + Create a color using its red, green and blue components. Each + is expressed as a byte value, from 0 (dark) to 255 (light). - An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be - appended to the base components. + An alpha value going from 0.0 (invisible) to 1.0 (opaque) can + be appended to the base components. - .. function:: Color(Color.Type.RGB, profile, red, green, blue, """ \ - """alpha = 1.0) + .. function:: Color(Color.Type.RGB, profile, red, green, """ \ + """blue, alpha = 1.0) - Create a color using its profile, red, green and blue component. - Each is expressed as a byte value, from 0 (dark) to 255 (light). + Create a color using its profile, red, green and blue + component. Each is expressed as a byte value, from 0 (dark) to + 255 (light). The profile is one of the :class:`thcolor.Color.Profile` constants, and represents the RGB profile. - An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be - appended to the base components. + An alpha value going from 0.0 (invisible) to 1.0 (opaque) can + be appended to the base components. - .. function:: Color(Color.Type.HSL, hue, saturation, lightness, """ \ - """alpha = 1.0) + .. function:: Color(Color.Type.HSL, hue, saturation, """ \ + """lightness, alpha = 1.0) - Create a color using its hue, saturation and lightness components. - The hue is represented by an :class:`Angle` object, and the - saturation and lightness are values going from 0.0 to 1.0. + Create a color using its hue, saturation and lightness + components. The hue is represented by an :class:`Angle` object, + and the saturation and lightness are values going from 0.0 to + 1.0. - An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be - appended to the base components. + An alpha value going from 0.0 (invisible) to 1.0 (opaque) can + be appended to the base components. - .. function:: Color(Color.Type.HWB, hue, whiteness, blackness, """ \ - """alpha = 1.0) + .. function:: Color(Color.Type.HWB, hue, whiteness, """ \ + """blackness, alpha = 1.0) - Create a color using its hue, whiteness and blackness components. - The hue is represented by an :class:`Angle` object, and the - whiteness and lightness are values going from 0.0 to 1.0. + Create a color using its hue, whiteness and blackness + components. The hue is represented by an :class:`Angle` object, + and the whiteness and lightness are values going from 0.0 to + 1.0. - An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be - appended to the base components. + An alpha value going from 0.0 (invisible) to 1.0 (opaque) can + be appended to the base components. .. function:: Color(Color.Type.CMYK, cyan, magenta, yellow, """ \ """black, alpha = 1.0) @@ -192,35 +202,35 @@ class Color: Create a color using its cyan, magenta, yellow and blackness components, which are all values going from 0.0 to 1.0. - An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be - appended to the base components. + An alpha value going from 0.0 (invisible) to 1.0 (opaque) can + be appended to the base components. .. function:: Color(Color.Type.LAB, lightness, a, b, alpha = 1.0) - Create a color using its CIE Lightness (similar to the lightness - in the HSL representation) and the A and B axises in the Lab - colorspace, represented by signed numbers. + Create a color using its CIE Lightness (similar to the + lightness in the HSL representation) and the A and B axises in + the Lab colorspace, represented by signed numbers. - An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be - appended to the base components. + An alpha value going from 0.0 (invisible) to 1.0 (opaque) can + be appended to the base components. .. function:: Color(Color.Type.LCH, lightness, chroma, hue, """ \ """alpha = 1.0) - Create a color using its CIE Lightness (similar to the lightness - in the HSL representation), its chroma (as a positive number - theoretically unbounded) and its hue. + Create a color using its CIE Lightness (similar to the + lightness in the HSL representation), its chroma (as a positive + number theoretically unbounded) and its hue. - An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be - appended to the base components. + An alpha value going from 0.0 (invisible) to 1.0 (opaque) can + be appended to the base components. .. function:: Color(Color.Type.XYZ, x, y, z, alpha = 1.0) Create a color using its CIE XYZ components (as numbers between 0 and 1). - An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be - appended to the base components. """ + An alpha value going from 0.0 (invisible) to 1.0 (opaque) can + be appended to the base components. """ class Type(_Enum): """ Class representing the type of a color, or how it is expressed. @@ -237,18 +247,18 @@ class Color: .. data:: HSL - A color expressed through its HSL components: hue, saturation - and lightness. + A color expressed through its HSL components: hue, + saturation and lightness. .. data:: HWB - A color expressed through its HWB components: hue, whiteness - and blackness. + A color expressed through its HWB components: hue, + whiteness and blackness. .. data:: CMYK - A color expressed through its CMYK components: cyan, magenta, - yellow and black. + A color expressed through its CMYK components: cyan, + magenta, yellow and black. .. data:: LAB @@ -270,18 +280,17 @@ class Color: # going up to 65535, just in case. INVALID = 65600 - - RGB = 65601 - HSL = 65602 - HWB = 65603 - CMYK = 65604 - LAB = 65605 - LCH = 65606 - XYZ = 65607 + RGB = 65601 + HSL = 65602 + HWB = 65603 + CMYK = 65604 + LAB = 65605 + LCH = 65606 + XYZ = 65607 class Profile(_Enum): - """ Class representing the profile of a color, or how it is expressed. - The following profiles are available: + """ Class representing the profile of a color, or how it is + expressed. The following profiles are available: .. data:: SRGB @@ -310,26 +319,26 @@ class Color: Level 4 <https://drafts.csswg.org/css-color/#valdef-color-rec2020>`_. """ - SRGB = 65700 - IMAGE_P3 = 65701 - A98RGB = 65702 + SRGB = 65700 + IMAGE_P3 = 65701 + A98RGB = 65702 PROPHOTORGB = 65703 - REC2020 = 65704 + REC2020 = 65704 def from_value(value): _profiles = { - 'srgb': 'SRGB', - 'imagep3': 'IMAGE_P3', - 'a98rgb': 'A98RGB', + 'srgb': 'SRGB', + 'imagep3': 'IMAGE_P3', + 'a98rgb': 'A98RGB', 'prophotorgb': 'PROPHOTORGB', - 'rec2020': 'REC2020'} + 'rec2020': 'REC2020'} if type(value) == str: - newval = ''.join(c for c in value.casefold() if c in \ - '0123456789abcdefghijklmnopqrstuvwxyz') + newval = ''.join(c for c in value.casefold() if c + in '0123456789abcdefghijklmnopqrstuvwxyz') try: value = _profiles[newval] - except: + except KeyError: pass return getattr(Color.Profile, value) @@ -370,7 +379,7 @@ class Color: def __init__(self, *args, **kwargs): self._type = Color.Type.INVALID - self.set(*args, **kwargs) + self._set(*args, **kwargs) def __repr__(self): args = (('type', f'{self.__class__.__name__}.{str(self._type)}'),) @@ -380,14 +389,17 @@ class Color: ('red', repr(self._r)), ('green', repr(self._g)), ('blue', repr(self._b))) elif self._type == Color.Type.HSL: - args += (('hue', repr(self._hue)), ('saturation', repr(self._sat)), + args += (('hue', repr(self._hue)), + ('saturation', repr(self._sat)), ('lightness', repr(self._lgt))) elif self._type == Color.Type.HWB: - args += (('hue', repr(self._hue)), ('whiteness', repr(self._wht)), + args += (('hue', repr(self._hue)), + ('whiteness', repr(self._wht)), ('blackness', repr(self._blk))) elif self._type == Color.Type.CMYK: - args += (('cyan', repr(self._cy)), ('magenta', repr(self._ma)), - ('yellow', repr(self._ye)), ('black', repr(self._bl))) + args += (('cyan', repr(self._cy)), + ('magenta', repr(self._ma)), ('yellow', repr(self._ye)), + ('black', repr(self._bl))) elif self._type == Color.Type.LAB: args += (('lightness', repr(self._lgt)), ('a', repr(self._a)), ('b', repr(self._b))) @@ -428,7 +440,13 @@ class Color: # Management methods. # --- - def set(self, *args, **kwargs): + def _setalpha(self, alpha): + """ Internal method for setting the alpha value. """ + + alpha = _percentage('alpha', alpha) + self._alpha = alpha + + def _set(self, *args, **kwargs): """ Set the color using its constructor arguments and keyword arguments. """ @@ -460,7 +478,7 @@ class Color: for name in names: if name in kwargs: if value is _UNDEFINED and args: - raise TypeError(f"{self.__class__.__name__}() " \ + raise TypeError(f"{self.__class__.__name__}() " f"got multiple values for argument {name}") raw_result = kwargs.pop(name) @@ -472,8 +490,8 @@ class Color: elif args: raw_result = args.pop(0) else: - raise TypeError(f"{self.__class__.__name__}() " \ - "missing a required positional argument: " \ + raise TypeError(f"{self.__class__.__name__}() " + "missing a required positional argument: " f"{name}") result = convert_func(name, raw_result) @@ -484,8 +502,8 @@ class Color: # Check for keyword arguments for which keys are not in the set. if kwargs: - raise TypeError(f"{next(iter(kwargs.keys()))} is an invalid " \ - f"keyword argument for type {type}") + raise TypeError(f"{next(iter(kwargs.keys()))} is an " + f"invalid keyword argument for type {type}") return results @@ -498,74 +516,75 @@ class Color: if args: try: type = kwargs.pop('type') - except: + except KeyError: type = args.pop(0) else: if isinstance(args[0], Color.Type): - raise TypeError(f"{self.__class__.__name__}() got " \ + raise TypeError(f"{self.__class__.__name__}() got " "multiple values for argument 'type'") else: try: type = kwargs.pop('type') - except: + except KeyError: type = self._type if type == Color.Type.INVALID: - raise TypeError(f"{self.__class__.__name__}() missing " \ + raise TypeError(f"{self.__class__.__name__}() missing " "required argument: 'type'") try: type = Color.Type(type) - except: + except ValueError: type = Color.Type.RGB # Initialize the properties. if type == Color.Type.RGB: self._profile, self._r, self._g, self._b, self._alpha = \ - _decode_varargs(\ - (('profile', 'p'), _color_profile, 'srgb'), - (('red', 'r'), _byte), - (('green', 'g'), _byte), - (('blue', 'b'), _byte), - (('alpha', 'a'), _percentage, 1.0)) + _decode_varargs( + (('profile', 'p'), _color_profile, 'srgb'), + (('red', 'r'), _byte), + (('green', 'g'), _byte), + (('blue', 'b'), _byte), + (('alpha', 'a'), _percentage, 1.0)) if self._profile != Color.Profile.SRGB: - raise NotImplementedError("rgb profile " \ + raise NotImplementedError("rgb profile " f"{repr(self._profile)} isn't managed yet.") elif type == Color.Type.HSL: - self._hue, self._sat, self._lgt, self._alpha = _decode_varargs(\ + self._hue, self._sat, self._lgt, self._alpha = _decode_varargs( (('hue', 'h'), _hue), (('saturation', 'sat', 's'), _percentage), (('lightness', 'light', 'lig', 'l'), _percentage), (('alpha', 'a'), _percentage, 1.0)) elif type == Color.Type.HWB: - self._hue, self._wht, self._blk, self._alpha = _decode_varargs(\ + self._hue, self._wht, self._blk, self._alpha = _decode_varargs( (('hue', 'h'), _hue), (('whiteness', 'white', 'w'), _percentage), (('blackness', 'black', 'b'), _percentage), (('alpha', 'a'), _percentage, 1.0)) elif type == Color.Type.CMYK: self._cy, self._ma, self._ye, self._bl, self._alpha = \ - _decode_varargs(\ - (('cyan', 'c'), _percentage), - (('magenta', 'm'), _percentage), - (('yellow', 'y'), _percentage), - (('black', 'b'), _percentage), - (('alpha', 'a'), _percentage, 1.0)) + _decode_varargs( + (('cyan', 'c'), _percentage), + (('magenta', 'm'), _percentage), + (('yellow', 'y'), _percentage), + (('black', 'b'), _percentage), + (('alpha', 'a'), _percentage, 1.0)) elif type == Color.Type.LAB: - self._lgt, self._a, self._b, self._alpha = _decode_varargs(\ - (('lightness', 'light', 'lig', 'l'), _unrestricted_percentage), - (('a',), _signed), - (('b',), _signed), - (('alpha', 'a'), _percentage, 1.0)) + self._lgt, self._a, self._b, self._alpha = _decode_varargs( + (('lightness', 'light', 'lig', 'l'), + _unrestricted_percentage), + (('a',), _signed), + (('b',), _signed), + (('alpha', 'a'), _percentage, 1.0)) elif type == Color.Type.LCH: - self._lgt, self._chr, self._hue, self._alpha = _decode_varargs(\ + self._lgt, self._chr, self._hue, self._alpha = _decode_varargs( (('lightness', 'light', 'lig', 'l'), _percentage), (('chroma', 'chr', 'c'), _unsigned), (('hue', 'h'), _hue), (('alpha', 'a'), _percentage, 1.0)) elif type == Color.Type.XYZ: - self._x, self._y, self._z, self._alpha = _decode_varargs(\ + self._x, self._y, self._z, self._alpha = _decode_varargs( (('x',), _percentage), (('y',), _percentage), (('z',), _percentage), @@ -593,6 +612,17 @@ class Color: # Conversion methods. # --- + def with_alpha(self, alpha): + """ Get the same color with a different alpha value. + For example: + + >>> Color.from_text("#876543").with_alpha(0.2).rgba() + ... (135, 101, 67, 0.2) """ + + color = _copy(self) + color._setalpha(alpha) + return color + def rgb(self): """ Get the sRGB (red, green, blue) components of the color. For example: @@ -614,16 +644,19 @@ class Color: elif self._type == Color.Type.LAB: return _lab_to_rgb(self._lgt, self._a, self._b) elif self._type == Color.Type.LCH: - return _lch_to_rgb(self._lgt, self._chr, self._hue) + return _lab_to_rgb(_lch_to_lab(self._lgt, self._chr, + self._hue)) - raise ValueError(f"color type {self._type} doesn't translate to rgb") + raise ValueError(f"color type {self._type} doesn't translate " + f"to rgb") def hsl(self): - """ Get the HSL (hue, saturation, lightness) components of the color. - For example: + """ Get the HSL (hue, saturation, lightness) components of the + color. For example: >>> Color.from_text("hsl(90turn 0% 5%)").hls() - ... (Angle(type = Angle.Type.TURN, value = 90.0), 0.05, 0.0) + ... (Angle(type = Angle.Type.TURN, value = 90.0), """ \ + """0.05, 0.0) If the color is not represented as HSL internally, it will be converted. """ @@ -634,14 +667,14 @@ class Color: try: rgb = self.rgb() except ValueError: - raise ValueError(f"color type {self._type} doesn't translate " \ + raise ValueError(f"color type {self._type} doesn't translate " "to hsl") from None return _rgb_to_hls(*rgb) def hwb(self): - """ Get the HWB (hue, whiteness, blackness) components of the color. - For example: + """ Get the HWB (hue, whiteness, blackness) components of the + color. For example: >>> Color.from_text("hwb(.7 turn / 5% 10%)").hwb() ... (Angle(type = Angle.Type.TURN, value = 0.7), 0.05, 0.1) @@ -655,7 +688,7 @@ class Color: try: rgb = self.rgb() except ValueError: - raise ValueError(f"color type {self._type} doesn't translate " \ + raise ValueError(f"color type {self._type} doesn't translate " "to hwb") from None return _rgb_to_hwb(*rgb) @@ -676,7 +709,7 @@ class Color: try: rgb = self.rgb() except ValueError: - raise ValueError(f"color type {self._type} doesn't translate " \ + raise ValueError(f"color type {self._type} doesn't translate " "to cmyk") from None return _rgb_to_cmyk(*rgb) @@ -699,7 +732,7 @@ class Color: try: rgb = self.rgb() except ValueError: - raise ValueError(f"color type {self._type} doesn't translate " \ + raise ValueError(f"color type {self._type} doesn't translate " "to lab") from None return _rgb_to_lab(*rgb) @@ -720,7 +753,7 @@ class Color: try: lab = self.lab() except ValueError: - raise ValueError(f"color type {self._type} doesn't translate " \ + raise ValueError(f"color type {self._type} doesn't translate " "to lch") from None return _lab_to_lch(*lab) @@ -741,8 +774,8 @@ class Color: raise NotImplementedError # TODO def rgba(self): - """ Get the sRGB (red, green, blue) and alpha components of the color. - For example: + """ Get the sRGB (red, green, blue) and alpha components of the + color. For example: >>> Color.from_text("#87654321").rgb() ... (135, 101, 67, 0.1294) @@ -756,40 +789,42 @@ class Color: return (r, g, b, alpha) def hsla(self): - """ Get the HSL (hue, saturation, lightness) and alpha components of - the color. For example: + """ Get the HSL (hue, saturation, lightness) and alpha components + of the color. For example: >>> Color.from_text("hsl(90turn 0% 5% .8)").hlsa() - ... (Angle(type = Angle.Type.TURN, value = 90.0), 0.05, 0.0, 0.8) + ... (Angle(type = Angle.Type.TURN, value = 90.0), """ \ + """0.05, 0.0, 0.8) If the color is not represented as HSL internally, it will be converted. """ - h, s, l = self.hsl() + hue, saturation, lightness = self.hsl() alpha = self._alpha - return (h, s, l, alpha) + return (hue, saturation, lightness, alpha) def hls(self): """ Alias for :meth:`hsl` but reverses the lightness and saturation for commodity. """ - h, s, l = self.hsl() - return (h, l, s) + hue, saturation, lightness = self.hsl() + return (hue, lightness, saturation) def hlsa(self): """ Alias for :meth:`hsla` but reverses the lightness and saturation for commodity. """ - h, s, l, a = self.hsla() - return (h, l, s, a) + hue, saturation, lightness, alpha = self.hsla() + return (hue, lightness, saturation, alpha) def hwba(self): """ Get the HWB (hue, whiteness, blackness) and alpha components of the color. For example: >>> Color.from_text("hwb(.7 turn / 5% 10% .2)").hwba() - ... (Angle(type = Angle.Type.TURN, value = 0.7), 0.05, 0.1, 0.2) + ... (Angle(type = Angle.Type.TURN, value = 0.7), """ \ + """0.05, 0.1, 0.2) If the color is not represented as HSL internally, it will be converted. """ @@ -800,8 +835,8 @@ class Color: return (h, w, b, a) def cmyka(self): - """ Get the CMYK (cyan, magenta, yellow, black) and alpha components - of the color. For example: + """ Get the CMYK (cyan, magenta, yellow, black) and alpha + components of the color. For example: >>> Color.from_text("cmyk(.1 .2 .3 .4 / 10%)").cmyka() ... (0.1, 0.2, 0.3, 0.4, 0.1) @@ -857,7 +892,7 @@ class Color: x, y, z = self.xyz() alpha = self._alpha - return (x, y, z) + return (x, y, z, alpha) def css(self): """ Get the CSS color descriptions, with older CSS specifications @@ -921,14 +956,14 @@ class Color: def from_str(*args, **kwargs): """ Alias for :meth:`from_text()`. """ - return Color.from_text(value) + return Color.from_text(*args, **kwargs) def from_string(*args, **kwargs): """ Alias for :meth:`from_text()`. """ - return Color.from_text(value) + return Color.from_text(*args, **kwargs) - def from_text(expr, ref = None): + def from_text(expr, ref=None): """ Create a color from a string using a :class:`Reference` object. If the ``ref`` argument is ``None``, then the default reference is loaded. @@ -936,13 +971,14 @@ class Color: An example: >>> Color.from_text("#123456") - ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \ - """ blue = 86, alpha = 1.0) """ + ... Color(type = Color.Type.RGB, red = 18, """ \ + """green = 52, blue = 86, alpha = 1.0) """ if ref is None: ref = _Reference.default() if not isinstance(ref, _Reference): - raise ValueError("ref is expected to be a subclass of Reference") + raise ValueError("ref is expected to be a subclass " + "of Reference") class argument: def __init__(self, column, value): @@ -950,8 +986,9 @@ class Color: self._value = value def __repr__(self): - return f"{self.__class__.__name__}(column = {self._column}, " \ - f"value = {repr(self._value)})" + return (f"{self.__class__.__name__}" + f"(column = {self._column}, " + f"value = {repr(self._value)})") @property def column(self): @@ -965,6 +1002,10 @@ class Color: if not match: return () + args = recurse(column + match.start('nextargs'), + _get_color_pattern().fullmatch(match['nextargs'] or "")) + value = None + if match['agl_val'] is not None: # The matched value is an angle. @@ -974,17 +1015,17 @@ class Color: 'rad': _Angle.Type.RAD, 'turn': _Angle.Type.TURN}[match['agl_typ']] - value = _Reference.angle(_Angle(agl_typ, + value = _angle(_Angle(agl_typ, float(match['agl_val']))) elif match['per'] is not None: # The matched value is a percentage. value = float(match['per']) - value = _Reference.percentage(value) + value = _percentage(value) elif match['num'] is not None: # The matched value is a number. - value = _Reference.number(match['num']) + value = _number(match['num']) elif match['hex'] is not None: # The matched value is a hex color. @@ -998,7 +1039,7 @@ class Color: b = int(name[4:6], 16) a = int(name[6:8], 16) / 255.0 if len(name) == 8 else 1.0 - value = _Reference.color(Color(Color.Type.RGB, r, g, b, a)) + value = _color(Color(Color.Type.RGB, r, g, b, a)) elif match['arg'] is not None: # The matched value is a function. @@ -1014,36 +1055,37 @@ class Color: try: func = ref.functions[name] except KeyError: - raise _ColorExpressionDecodingError("no such function " \ - f"{repr(name)}", column = column) + raise _ColorExpressionDecodingError("no such function " + f"{repr(name)}", column=column) try: value = func(*map(lambda x: x.value, args)) except _NotEnoughArgumentsError as e: - raise _ColorExpressionDecodingError("not enough " \ - f"arguments (expected at least {e.count} arguments)", - column = column, func = name) + raise _ColorExpressionDecodingError("not enough " + f"arguments (expected at least {e.count} " + "arguments)", + column=column, func=name) except _TooManyArgumentsError as e: - raise _ColorExpressionDecodingError("extraneous " \ + raise _ColorExpressionDecodingError("extraneous " f"argument (expected {e.count} arguments at most)", - column = args[e.count].column, func = name) + column=args[e.count].column, func=name) except _InvalidArgumentTypeError as e: - raise _ColorExpressionDecodingError("type mismatch for " \ - f"argument {e.index + 1}: expected {e.expected}, " \ - f"got {e.got}", column = args[e.index].column, - func = name) + raise _ColorExpressionDecodingError("type mismatch " + f"for argument {e.index + 1}: " + f"expected {e.expected}, got {e.got}", + column=args[e.index].column, func=name) except _InvalidArgumentValueError as e: - raise _ColorExpressionDecodingError("erroneous value " \ + raise _ColorExpressionDecodingError("erroneous value " f"for argument {e.index + 1}: {e.text}", - column = args[e.index].column, func = name) + column=args[e.index].column, func=name) except NotImplementedError: raise _ColorExpressionDecodingError("not implemented", - column = column, func = name) + column=column, func=name) else: if match['ncol']: - # The match is probably a natural color (ncol), we ought - # to parse it and get the following arguments or, if - # anything is invalid, to treat it as a color name. + # The match is probably a natural color (ncol), we + # ought to parse it and get the following arguments or, + # if anything is invalid, to treat it as a color name. name = match['ncol'] @@ -1055,15 +1097,12 @@ class Color: if number >= 0 and number < 100: # Get the following arguments and check. - args = recurse(column + match.start('nextargs'), - _get_color_pattern().fullmatch(match['nextargs'] \ - or "")) - try: assert len(args) >= 2 w = args[0].value.to_factor() b = args[1].value.to_factor() - except: + except (AssertionError, TypeError, ValueError, + AttributeError): w = 0 b = 0 else: @@ -1071,33 +1110,30 @@ class Color: # Calculate the color and return the args. + angle_base = 'RYGCBM'.find(letter) * 60 \ + + number / 100 * 60 color = Color(Color.Type.HWB, - _Angle(_Angle.Type.DEG, 'RYGCBM'.find(letter) \ - * 60 + number / 100 * 60), w, b) + _Angle(_Angle.Type.DEG, angle_base, w, b)) - # And finally, return the args. + value = _color(color) - return (argument(column, _Reference.color(color)),) \ - + args + if value is None: + # The matched value is a named color. - # The matched value is a named color. + name = match['name'] - name = match['name'] + try: + # Get the named color (e.g. 'blue'). - try: - # Get the named color (e.g. 'blue'). - - value = ref.colors[name] - assert value != None - except: - r, g, b = _netscape_color(name) - value = Color(Color.Type.RGB, r, g, b, 1.0) + value = ref.colors[name] + assert value is not None + except AssertionError: + r, g, b = _netscape_color(name) + value = Color(Color.Type.RGB, r, g, b, 1.0) - value = _Reference.color(value) + value = _color(value) - return (argument(column, value),) \ - + recurse(column + match.start('nextargs'), - _get_color_pattern().fullmatch(match['nextargs'] or "")) + return ((argument(column, value),) + args) # Strip the expression. @@ -1108,9 +1144,10 @@ class Color: # Match the expression (and check it as a whole directly). - match = _get_color_pattern().fullmatch(expr) + match = _color_pattern.fullmatch(expr) if match is None: - raise _ColorExpressionDecodingError("expression parsing failed") + raise _ColorExpressionDecodingError("expression parsing " + "failed") # Get the result and check its type. @@ -1124,7 +1161,7 @@ class Color: result = ref.colors[result] except AttributeError: raise _ColorExpressionDecodingError("expected a color", - column = column) + column=column) return result diff --git a/thcolor/_ref.py b/thcolor/_ref.py index 8a39cbb..9c719a8 100755 --- a/thcolor/_ref.py +++ b/thcolor/_ref.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 -#****************************************************************************** +#************************************************************************** # Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** +#************************************************************************** """ Named color reference, parent class. """ -from inspect import (getfullargspec as _getfullargspec, - getmembers as _getmembers, ismethod as _ismethod) +from copy import copy as _copy +from inspect import (getfullargspec as _getfullargspec, getmro as _getmro, + getmembers as _getmembers, ismethod as _ismethod, + isfunction as _isfunction) from itertools import count as _count from warnings import warn as _warn @@ -21,6 +23,7 @@ __all__ = ["Reference"] _default_reference = None _color_cls = None + def _get_color_class(): global _color_cls @@ -31,249 +34,63 @@ def _get_color_class(): _color_cls = Color return _color_cls -class _type_or: - """ A type or another. """ - - def __init__(self, type1, type2): - self._type1 = type1 - self._type2 = type2 - - @property - def type1(self): - return self._type1 - - @property - def type2(self): - return self._type2 - - def __repr__(self): - return f"{repr(self._type1)} | {repr(self._type2)}" - - def __str__(self): - return f"{self._type1} or {self._type2}" - - def __or__(self, other): - return type_or(self, other) - - def __contains__(self, other): - return other in self._type1 or other in self._type2 # --- # Main reference definition. # --- + class Reference: """ Function reference for color parsing and operations. """ - def __init__(self): - pass - - # --- - # Base type and function definitions for parsing. - # --- - - class base_type(type): - """ The metaclass for all types used below. """ - - def __new__(mcls, name, bases, attrs): - return super().__new__(mcls, name, bases, attrs) - - def __init__(self, name, bases, attrs): - self.__name = name - - def __contains__(self, other): - return self if other == self else None - - def __or__(self, other): - return _type_or(self, other) - - def __repr__(self): - return f"<class {repr(self.__name)}>" - - def __str__(self): - return self.__name - - class number(metaclass = base_type): - """ Syntaxical element for expression decoding, representing - a number, integer or decimal, positive or negative. - - This element is usually used to represent a byte value, - a factor (usually from 0.0 to 1.0), a color or an angle - in degrees. """ - - def __init__(self, value): - if type(value) == str: - self._strvalue = value - self._value = float(value) - else: - self._value = float(value) - if self._value == int(self._value): - self._strvalue = str(int(self._value)) - else: - self._strvalue = str(value) - - def __str__(self): - return self._strvalue - - @property - def value(self): - return self._value - - def to_byte(self): - """ Make a byte (from 0 to 255) out of the number. """ - - try: - value = int(self._value) - - assert value == self._value - assert 0 <= value < 256 - except: - raise ValueError("unsuitable value for byte conversion: " \ - f"{repr(self._value)}") - - return value - - def to_factor(self, min = 0.0, max = 1.0): - """ Make a factor (usually from 0.0 to 1.0) out of the number. """ - - if (min is not None and self._value < min) \ - or (max is not None and self._value > max): - if max is None: - msg = f"above {min}" - elif min is None: - msg = f"under {max}" - else: - msg = f"between {min} and {max}" - - raise ValueError(f"expected a value {msg}, got {self._value}") - - try: - assert 0.0 <= self._value <= 1.0 - except: - raise ValueError("expected a value between 0.0 and 1.0, got " \ - f"{self._value}") - - return self._value + def __getattr__(self, name): + def pred(x): + return _isfunction(x) | _ismethod(x) - def to_hue(self): - """ Make an angle in degrees out of the number. """ + for member_name in dir(self): + # Check if it's an interesting member. - return _Angle(_Angle.Type.DEG, self._value) + if member_name[:1] == '_': + continue - class percentage(metaclass = base_type): - """ Syntaxical element for expression decoding, representing - a percentage (number followed by a '%' sign). + member = getattr(self, member_name) + if not _ismethod(member) and not _isfunction(member): + continue - This element is usually used to represent a factor (usually - from 0% to 100%) or anything that can be factored such as a - byte value (where 0% represents 0 and 100% represents 255). """ + # Access the aliases through the mro using self and super. - def __init__(self, value): - self._value = value + for obj in (self, super()): + # Check the aliases. - if value < 0: - value = 0 - - # value can actually be more than 100. - - def __repr__(self): - return f"{self._value} %" - - def to_factor(self, min = 0.0, max = 1.0): - """ Make a factor (usually from 0.0 to 1.0) out of the number. """ + try: + aliases = getattr(obj, member_name)._ref_aliases + except (AttributeError, AssertionError): + continue - value = self._value / 100 - if (min is not None and value < min) \ - or (max is not None and value > max): - if max is None: - msg = f"above {min * 100}%" - elif min is None: - msg = f"under {max * 100}%" - else: - msg = f"between {min * 100}% and {max * 100}%" + for alias_name, alias in aliases: + if alias_name == name: + # XXX: test + print(alias(self.number(1), self.number(2), + self.number(3))) + return alias - raise ValueError(f"expected a percentage {msg}, " \ - f"got {self._value}%") + # Explore the aliases in a recursive way. + try: + value = super().__getattr__(name) return value + except AttributeError: + pass - def to_byte(self): - """ Make a byte (from 0 to 255) out of the number. """ - - if self._value < 0: - self._value = 0 - if self._value > 100: - self._value = 100 - - return int(min(self._value / 100, 1.0) * 255) - - class angle(metaclass = base_type): - """ Syntaxical element for expression decoding, representing - an angle (a number followed by an angle unit: degrees, gradiants, - radiants or turns). """ - - def __init__(self, value): - if not isinstance(value, _Angle): - raise TypeError("expected an Angle instance") - - self._value = value - - def __repr__(self): - return repr(self._value) - - def to_hue(self): - """ Get the :class:`thcolor.Angle` object. """ - - return self._value - - class color(metaclass = base_type): - """ Syntaxical element for expression decoding, representing - a color using several methods: - - - a hexadecimal code preceeded by a '#', e.g. "#123ABC". - - a natural color (see - `NCol <https://www.w3schools.com/colors/colors_ncol.asp>`_). - - a color name as defined by a :class:`Reference`. - - a legacy color name using the legacy color algorithm (or - 'Netscape' color algorithm). """ - - def __init__(self, value): - if not isinstance(value, _get_color_class()): - raise ValueError("expected a Color instance") - - self._value = value - - def __repr__(self): - return repr(self._value) - - def to_color(self): - """ Get the :class:`thcolor.Color` object. """ - - return self._value - - # --- - # Function helper. - # --- - - def alias(*names): - """ Decorator for aliasing with another name. Defines a lot, you - know. """ - - def _decorator(func): - if not hasattr(func, '_ref_aliases'): - func._ref_aliases = () - - func._ref_aliases += names - return func - - return _decorator + raise AttributeError(repr(name)) from None # --- # Function and named color getters. # --- def _get_functions(self): - """ Function getter, which can be used as ``.functions[name](args…)``. + """ Function getter, which can be used as + ``.functions[name](args…)``. Provides a wrapper to the method which checks the argument types and perform the necessary conversions before passing @@ -292,42 +109,18 @@ class Reference: def __getitem__(self, name): fref = self._fref - found = False - # First, check if the function name is valid and if the - # method exists. + # Get the method through __getattr__, who will handle the + # recursivity through parent classes up to Reference. - members = dict(_getmembers(fref, predicate = _ismethod)) - validname = lambda n: type(n) == str and n[0:1] != '_' \ - and n not in ('functions', 'named', 'default', 'alias') - - if validname(name): - try: - method = members[name] - found = True - except (KeyError, AssertionError): - pass - - # Then, if we haven't found the method, check the aliases. - - if not found: - for member in members.values(): - try: - aliases = member._ref_aliases - except (AttributeError, AssertionError): - continue - - if name not in aliases: - continue - method = member - found = True - break - - # If we still haven't found the method, well… time to raise - # the not found exception. - - if not found: - raise KeyError(repr(name)) + def validname(n): + return type(n) == str and n[0:1] != '_' and n \ + not in ('functions', 'named', 'default', 'alias') + try: + assert validname(name) + method = getattr(fref, name) + except (AssertionError, AttributeError): + raise KeyError(name) # Make a function separated from the class, copy the # annotations and add the type check on each argument. @@ -341,7 +134,7 @@ class Reference: self.__annotations__ = func.__annotations__ try: del self.__annotations__['self'] - except: + except AssertionError: pass spec = _getfullargspec(func) @@ -349,7 +142,7 @@ class Reference: def annotate(arg_name): try: return spec.annotations[arg_name] - except: + except (AttributeError, KeyError, IndexError): return None self._args = list(map(annotate, spec.args[1:])) @@ -387,7 +180,7 @@ class Reference: if Reference.color in exp: try: args[-1] = self._fref.colors[arg] - except: + except (IndexError, KeyError): pass else: continue @@ -406,12 +199,12 @@ class Reference: For example, with a classical CSS reference named ``ref``: >>> ref.colors['blue'] - ... Color(type = Color.Type.RGB, red = 0, green = 0, """ \ - """blue = 255, alpha = 1.0) + ... Color(type = Color.Type.RGB, red = 0, """ \ + """green = 0, blue = 255, alpha = 1.0) >>> ref.colors[thcolor.Reference.color(""" \ """thcolor.Color.from_text('#123456'))] - ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \ - """blue = 86, alpha = 1.0) """ + ... Color(type = Color.Type.RGB, red = 18, """ \ + """green = 52, blue = 86, alpha = 1.0) """ class _ColorGetter: def __init__(self, ref): @@ -425,7 +218,7 @@ class Reference: try: name = str(key) - except: + except (TypeError, ValueError): raise KeyError(repr(key)) try: @@ -433,23 +226,24 @@ class Reference: except KeyError: pass except Exception as e: - _warn(RuntimeWarning, f"{self.__class__.__name__} " \ - f"returned exception {e.__class__.__name__} instead " \ - f"of KeyError for color name {repr(name)}.") + _warn(RuntimeWarning, f"{self.__class__.__name__} " + f"returned exception {e.__class__.__name__} " + f"instead of KeyError for color name " + f"{repr(name)}.") pass else: try: assert isinstance(value, _get_color_class()) except AssertionError: - _warn(RuntimeWarning, f"{self.__class__.__name__} " \ - f"returned non-Color value {repr(value)} for " \ + _warn(RuntimeWarning, f"{self.__class__.__name__} " + f"returned non-Color value {repr(value)} for " f"color name {repr(name)}, ignoring.") else: return value try: r, g, b = _netscape_color(name) - except: + except (TypeError, ValueError): pass else: Color = _get_color_class() diff --git a/thcolor/_sys.py b/thcolor/_sys.py deleted file mode 100755 index 014270a..0000000 --- a/thcolor/_sys.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env python3 -#****************************************************************************** -# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> -# This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** -""" Conversions between color systems. """ - -from math import (ceil as _ceil, atan2 as _atan2, sqrt as _sqrt, cos as _cos, - sin as _sin, pow as _pow) - -from ._angle import Angle as _Angle - -__all__ = ["hls_to_rgb", "rgb_to_hls", "rgb_to_hwb", "hwb_to_rgb", - "cmyk_to_rgb", "rgb_to_cmyk", "lab_to_rgb", "rgb_to_lab", - "lch_to_lab", "lab_to_lch", "netscape_color"] - -# --- -# Color systems conversion utilities. -# --- - -def _rgb(r, g, b): - return tuple(map(lambda x: int(round(x * 255, 0)), (r, g, b))) -def _hls(hue, s, l): - return _Angle(_Angle.Type.DEG, round(hue, 2)), round(l, 2), round(s, 2) - -def _rgb_to_lrgb(r, g, b): - def _linearize(val): - # Undo gamma encoding. - - val /= 255 - if val < 0.04045: - return val / 12.92 - - return _pow((val + 0.055) / 1.055, 2.4) - - return tuple(map(_linearize, (r, g, b))) - -def hls_to_rgb(hue, l, s): - """ Convert HLS to RGB. """ - - if s == 0: - # Achromatic color. - - return l, l, l - - def _hue_to_rgb(t1, t2, hue): - hue %= 6 - - if hue < 1: - return t1 + (t2 - t1) * hue - elif hue < 3: - return t2 - elif hue < 4: - return t1 + (t2 - t1) * (4 - hue) - return t1 - - hue = (hue.degrees % 360) / 60 - if l <= 0.5: - t2 = l * (s + 1) - else: - t2 = l + s - (l * s) - - t1 = l * 2 - t2 - - return _rgb(\ - _hue_to_rgb(t1, t2, hue + 2), - _hue_to_rgb(t1, t2, hue), - _hue_to_rgb(t1, t2, hue - 2)) - -def hwb_to_rgb(hue, w, bl): - """ Convert HWB to RGB color. - https://drafts.csswg.org/css-color/#hwb-to-rgb """ - - r, g, b = map(lambda x: x / 255, hls_to_rgb(hue, 0.5, 1.0)) - if w + bl > 1: - w, bl = map(lambda x: x / (w + bl), (w, bl)) - return _rgb(*map(lambda x: x * (1 - w - bl) + w, (r, g, b))) - -def cmyk_to_rgb(c, m, y, k): - """ Convert CMYK to RGB. """ - - r = 1 - min(1, c * (1 - k) + k) - g = 1 - min(1, m * (1 - k) + k) - b = 1 - min(1, y * (1 - k) + k) - - return _rgb(r, g, b) - -def lab_to_rgb(l, a, b): - """ Convert LAB to RGB. """ - - # TODO - raise NotImplementedError - -def rgb_to_lab(r, g, b): - """ Convert RGB to LAB. """ - - # TODO - raise NotImplementedError - -def lab_to_lch(l, a, b): - """ Convert RGB to LAB. """ - - h = _Angle(_Angle.Type.RAD, _atan2(b, a)) - c = _sqrt(a * a + b * b) - - return (l, c, h) - -def lch_to_lab(l, c, h): - """ Convert LCH to LAB. """ - - a = c * _cos(h.radians) - b = c * _sin(h.radians) - - return (l, a, b) - -def rgb_to_hls(r, g, b): - """ Convert RGB to HLS. """ - - r, g, b = map(lambda x: x / 255, (r, g, b)) - - min_value = min((r, g, b)) - max_value = max((r, g, b)) - chroma = max_value - min_value - - if chroma == 0: - hue = 0 - elif r == max_value: - hue = (g - b) / chroma - elif g == max_value: - hue = (b - r) / chroma + 2 - else: - hue = (r - g) / chroma + 4 - - hue = hue * 60 + (hue < 0) * 360 - l = (min_value + max_value) / 2 - if min_value == max_value: - s = 0 - else: - s = max_value - min_value - if l < 0.5: - s /= max_value + min_value - else: - s /= 2 - max_value - min_value - - return _hls(hue, l, s) - -def rgb_to_hwb(r, g, b): - """ Convert RGB to HWB. """ - - r, g, b = map(lambda x: x / 255, (r, g, b)) - - max_value = max((r, g, b)) - min_value = min((r, g, b)) - chroma = max_value - min_value - - if chroma == 0: - hue = 0 - elif r == max_value: - hue = (g - b) / chroma - elif g == max_value: - hue = (b - r) / chroma + 2 - elif g == max_value: - hue = (r - g) / chroma + 4 - - hue = (hue % 6) * 360 - w = min_value - b = max_value - - return _Angle(_Angle.Type.DEG, hue), w, b - -def rgb_to_cmyk(r, g, b): - """ Convert RGB to CMYK. """ - - r, g, b = map(lambda x: x / 255, (r, g, b)) - - k = 1 - max((r, g, b)) - if k == 1: - c, m, y = 0, 0, 0 - else: - c = (1 - r - k) / (1 - k) - m = (1 - g - k) / (1 - k) - y = (1 - b - k) / (1 - k) - - return (c, m, y, k) - -# --- -# Other utilities. -# --- - -def netscape_color(name): - """ Produce a color from a name (which can be all-text, all-digits or - both), using the Netscape behaviour. """ - - # Find more about this here: https://stackoverflow.com/a/8333464 - # - # First of all: - # - we sanitize our input by replacing invalid characters - # by '0' characters (the 0xFFFF limit is due to how - # UTF-16 was managed at the time). - # - we truncate our input to 128 characters. - - name = name.lower() - name = ''.join(c if c in '0123456789abcdef' \ - else ('0', '00')[ord(c) > 0xFFFF] \ - for c in name[:128])[:128] - - # Then we calculate some values we're going to need. - # `iv` is the size of the zone for a member. - # `sz` is the size of the digits slice to take in that zone - # (max. 8). - # `of` is the offset in the zone of the slice to take. - - iv = _ceil(len(name) / 3) - of = iv - 8 if iv > 8 else 0 - sz = iv - of - - # Then we isolate the slices using the values calculated - # above. `gr` will be an array of 3 or 4 digit strings - # (depending on the number of members). - - gr = list(map(lambda i: name[i * iv + of:i * iv + iv] \ - .ljust(sz, '0'), range(3))) - - # Check how many digits we can skip at the beginning of - # each slice. - - pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr)) - pre = min(pre, sz - 2) - - # Then we extract the values. - - it = map(lambda x: int('0' + x[pre:pre + 2], 16), gr) - r, g, b = it - - return (r, g, b) - -# End of file. diff --git a/thcolor/angles.py b/thcolor/angles.py new file mode 100644 index 0000000..8ad8703 --- /dev/null +++ b/thcolor/angles.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +#************************************************************************** +# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# This file is part of the thcolor project, which is MIT-licensed. +#************************************************************************** +""" Angle representation and conversions. """ + +from math import pi as _pi + +__all__ = ['Angle', + 'DegreesAngle', 'GradiansAngle', 'RadiansAngle', 'TurnsAngle'] + +class Angle: + """ Abstract class representing an angle within thcolor, used for + some color representations (most notably hue). """ + + __slots__ = () + + def __init__(self): + pass + + def __repr__(self): + params = ((key, getattr(self, key)) for key in dir(self) + if not key.startswith('_') and not callable(getattr(self, key))) + return (f"{self.__class__.__name__}(" + f"{', '.join(f'{key} = {repr(val)}' for key, val in params)})") + + def asdegrees(self) -> 'DegreesAngle': + """ Get the current angle as a degrees angle. """ + + try: + value = self._value + ob = self._bottom + ot = self._top + except AttributeError: + raise NotImplementedError from None + + nb = DegreesAngle._bottom + nt = DegreesAngle._top + + return DegreesAngle((value - ob) / (ot - ob) * (nt - nb) + nb) + + def asgradians(self) -> 'GradiansAngle': + """ Get the current angle as a gradians angle. """ + + try: + value = self._value + ob = self._bottom + ot = self._top + except AttributeError: + raise NotImplementedError from None + + nb = GradiansAngle._bottom + nt = GradiansAngle._top + + return GradiansAngle((value - ob) / (ot - ob) * (nt - nb) + nb) + + def asradians(self) -> 'RadiansAngle': + """ Get the current angle as a radians angle. """ + + try: + value = self._value + ob = self._bottom + ot = self._top + except AttributeError: + raise NotImplementedError from None + + nb = RadiansAngle._bottom + nt = RadiansAngle._top + + return RadiansAngle((value - ob) / (ot - ob) * (nt - nb) + nb) + + def asturns(self) -> 'TurnsAngle': + """ Get the current angle as a turns angle. """ + + try: + value = self._value + ob = self._bottom + ot = self._top + except AttributeError: + raise NotImplementedError from None + + nb = TurnsAngle._bottom + nt = TurnsAngle._top + + return TurnsAngle((value - ob) / (ot - ob) * (nt - nb) + nb) + + +class DegreesAngle(Angle): + """ An angle expressed in degrees. + + A 270° angle can be created the following way: + + .. code-block:: python + + angle = DegreesAngle(270) + + :param degrees: Degrees; canonical values are between 0 and 360 + excluded. """ + + __slots__ = ('_value') + + _bottom = 0 + _top = 360.0 + + def __init__(self, degrees: float): + self._value = float(degrees) # % 360.0 + + @property + def degrees(self) -> float: + """ Degrees. """ + + return self._value + + +class GradiansAngle(Angle): + """ An angle expressed in gradians. + + A 565.5 gradians angle can be created the following way: + + .. code-block:: python + + angle = GradiansAngle(565.5) + + :param gradians: Gradians; canonical values are between + 0 and 400.0 excluded.""" + + __slots__ = ('_value') + + _bottom = 0 + _top = 400.0 + + def __init__(self, gradians: float): + self._value = float(gradians) # % 400.0 + + @property + def gradians(self) -> float: + """ Gradians. """ + + return self._value + + +class RadiansAngle(Angle): + """ An angle expressed in radians. + + A π radians angle can be created the following way: + + .. code-block:: python + + from math import pi + angle = RadiansAngle(pi) + + :param radians: Radians; canonical are between 0 and 2π + excluded. """ + + __slots__ = ('_value') + + _bottom = 0 + _top = 2 * _pi + + def __init__(self, radians: float): + self._value = float(radians) # % (2 * _pi) + + def __repr__(self): + r = self.radians / _pi + ir = int(r) + if r == ir: + r = ir + + return f"{self.__class__.__name__}(radians = {f'{r}π' if r else '0'})" + + @property + def radians(self) -> float: + """ Radians. """ + + return self._value + + +class TurnsAngle(Angle): + """ An angle expressed in turns. + + A 3.5 turns angle can be created the following way: + + .. code-block:: python + + angle = TurnsAngle(3.5) + + :param turns: Turns; canonical values are between 0 and 1 + excluded. """ + + __slots__ = ('_value') + + _bottom = 0 + _top = 1 + + def __init__(self, turns: float): + self._value = float(turns) # % 1.0 + + @property + def turns(self) -> float: + """ Turns. """ + + return self._value + +# End of file. diff --git a/thcolor/builtin/__init__.py b/thcolor/builtin/__init__.py index c9bc0d2..b9ed556 100755 --- a/thcolor/builtin/__init__.py +++ b/thcolor/builtin/__init__.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -#****************************************************************************** +#************************************************************************** # Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** +#************************************************************************** """ Named colors references, using various sources. """ from ._css import (CSS1Reference, CSS2Reference, CSS3Reference, diff --git a/thcolor/builtin/_css.py b/thcolor/builtin/_css.py index ac9e434..457d560 100755 --- a/thcolor/builtin/_css.py +++ b/thcolor/builtin/_css.py @@ -1,18 +1,22 @@ #!/usr/bin/env python3 -#****************************************************************************** +#************************************************************************** # Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** -""" Named colors and function definitions. Color names are case-insensitive. +#************************************************************************** +""" Named colors and function definitions. Color names are + case-insensitive. Taken from: https://www.w3schools.com/cssref/css_colors.asp """ from .._color import Color as _Color from .._ref import Reference as _Reference from .._exc import InvalidArgumentValueError as _InvalidArgumentValueError +from ..syntax import (number as _number, percentage as _percentage, + angle as _angle, color as _color, alias as _alias) __all__ = ["CSS1Reference", "CSS2Reference", "CSS3Reference", "CSS4Reference"] + def _rgb(raw): r = int(raw[1:3], 16) g = int(raw[3:5], 16) @@ -20,373 +24,271 @@ def _rgb(raw): return _Color(_Color.Type.RGB, r, g, b) + class CSS1Reference(_Reference): """ Named colors from `CSS Level 1 <https://www.w3.org/TR/CSS1/>`_. """ - number = _Reference.number - percentage = _Reference.percentage - color = _Reference.color - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # --- - # Named colors. - # --- - - __colors = { - 'black': _rgb('#000000'), - 'silver': _rgb('#c0c0c0'), - 'gray': _rgb('#808080'), - 'white': _rgb('#ffffff'), - - 'maroon': _rgb('#800000'), - 'red': _rgb('#ff0000'), - 'purple': _rgb('#800080'), - 'fuchsia': _rgb('#ff00ff'), - 'green': _rgb('#008000'), - 'lime': _rgb('#00ff00'), - 'olive': _rgb('#808000'), - 'yellow': _rgb('#ffff00'), - 'navy': _rgb('#000080'), - 'blue': _rgb('#0000ff'), - 'teal': _rgb('#008080'), - 'aqua': _rgb('#00ffff')} - - def _color(self, name): - if name == 'transparent': - return _Color(_Color.Type.RGB, 0, 0, 0, 0) - - try: - return self.__colors[name] - except: - return super()._color(name) + black = _rgb('#000000') + silver = _rgb('#c0c0c0') + gray = _rgb('#808080') + white = _rgb('#ffffff') + + maroon = _rgb('#800000') + red = _rgb('#ff0000') + purple = _rgb('#800080') + fuchsia = _rgb('#ff00ff') + green = _rgb('#008000') + lime = _rgb('#00ff00') + olive = _rgb('#808000') + yellow = _rgb('#ffff00') + navy = _rgb('#000080') + blue = _rgb('#0000ff') + teal = _rgb('#008080') + aqua = _rgb('#00ffff') + + transparent = _RGBColor(0, 0, 0, 0) # --- - # Utilities. + # Functions. # --- - def _rgb(self, rgba, rgb_indexes): - r, g, b, alpha = rgba - ri, gi, bi = rgb_indexes - + def rgb(self, + r: _number | _percentage = _number(0), + g: _number | _percentage = _number(0), + b: _number | _percentage = _number(0)): try: r = r.to_byte() except ValueError as e: - raise _InvalidArgumentValueError(ri, str(e)) + raise _InvalidArgumentValueError(0, str(e)) try: g = g.to_byte() except ValueError as e: - raise _InvalidArgumentValueError(gi, str(e)) + raise _InvalidArgumentValueError(1, str(e)) try: b = b.to_byte() except ValueError as e: - raise _InvalidArgumentValueError(bi, str(e)) - - try: - alpha = alpha.to_factor() - except ValueError as e: - raise _InvalidArgumentValueError(3, str(e)) - - return _Reference.color(_Color(_Color.Type.RGB, r, g, b, alpha)) - - # --- - # Functions. - # --- + raise _InvalidArgumentValueError(2, str(e)) - def rgb(self, r: number | percentage, - g: number | percentage = number(0), - b: number | percentage = number(0)): - return self._rgb((r, g, b, 1.0), (0, 1, 2)) + return _Reference.color(_RGBColor(r / 255, g / 255, b / 255, 1.0)) class CSS2Reference(CSS1Reference): """ Named colors from `CSS Level 2 (Revision 1) <https://www.w3.org/TR/CSS2/>`_. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # --- - # Named colors. - # --- - - __colors = { - 'orange': _rgb('#ffa500')} - - def _color(self, name): - try: - return self.__colors[name] - except: - return super()._color(name) + orange = _rgb('#ffa500') class CSS3Reference(CSS2Reference): """ Named colors and functions from `CSS Color Module Level 3 <https://drafts.csswg.org/css-color-3/>`_. """ - number = _Reference.number - percentage = _Reference.percentage - angle = _Reference.angle - color = _Reference.color - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + darkblue = _rgb('#00008B') + mediumblue = _rgb('#0000CD') + darkgreen = _rgb('#006400') + darkcyan = _rgb('#008B8B') + deepskyblue = _rgb('#00BFFF') + darkturquoise = _rgb('#00CED1') + mediumspringgreen = _rgb('#00FA9A') + springgreen = _rgb('#00FF7F') + cyan = _rgb('#00FFFF') + midnightblue = _rgb('#191970') + dodgerblue = _rgb('#1E90FF') + lightseagreen = _rgb('#20B2AA') + forestgreen = _rgb('#228B22') + seagreen = _rgb('#2E8B57') + darkslategray = _rgb('#2F4F4F') + darkslategrey = _rgb('#2F4F4F') + limegreen = _rgb('#32CD32') + mediumseagreen = _rgb('#3CB371') + turquoise = _rgb('#40E0D0') + royalblue = _rgb('#4169E1') + steelblue = _rgb('#4682B4') + darkslateblue = _rgb('#483D8B') + mediumturquoise = _rgb('#48D1CC') + indigo = _rgb('#4B0082') + darkolivegreen = _rgb('#556B2F') + cadetblue = _rgb('#5F9EA0') + cornflowerblue = _rgb('#6495ED') + mediumaquamarine = _rgb('#66CDAA') + dimgray = _rgb('#696969') + dimgrey = _rgb('#696969') + slateblue = _rgb('#6A5ACD') + olivedrab = _rgb('#6B8E23') + slategray = _rgb('#708090') + slategrey = _rgb('#708090') + lightslategray = _rgb('#778899') + lightslategrey = _rgb('#778899') + mediumslateblue = _rgb('#7B68EE') + lawngreen = _rgb('#7CFC00') + chartreuse = _rgb('#7FFF00') + aquamarine = _rgb('#7FFFD4') + grey = _rgb('#808080') + skyblue = _rgb('#87CEEB') + lightskyblue = _rgb('#87CEFA') + blueviolet = _rgb('#8A2BE2') + darkred = _rgb('#8B0000') + darkmagenta = _rgb('#8B008B') + saddlebrown = _rgb('#8B4513') + darkseagreen = _rgb('#8FBC8F') + lightgreen = _rgb('#90EE90') + mediumpurple = _rgb('#9370DB') + darkviolet = _rgb('#9400D3') + palegreen = _rgb('#98FB98') + darkorchid = _rgb('#9932CC') + yellowgreen = _rgb('#9ACD32') + sienna = _rgb('#A0522D') + brown = _rgb('#A52A2A') + darkgray = _rgb('#A9A9A9') + darkgrey = _rgb('#A9A9A9') + lightblue = _rgb('#ADD8E6') + greenyellow = _rgb('#ADFF2F') + paleturquoise = _rgb('#AFEEEE') + lightsteelblue = _rgb('#B0C4DE') + powderblue = _rgb('#B0E0E6') + firebrick = _rgb('#B22222') + darkgoldenrod = _rgb('#B8860B') + mediumorchid = _rgb('#BA55D3') + rosybrown = _rgb('#BC8F8F') + darkkhaki = _rgb('#BDB76B') + mediumvioletred = _rgb('#C71585') + indianred = _rgb('#CD5C5C') + peru = _rgb('#CD853F') + chocolate = _rgb('#D2691E') + tan = _rgb('#D2B48C') + lightgray = _rgb('#D3D3D3') + lightgrey = _rgb('#D3D3D3') + thistle = _rgb('#D8BFD8') + orchid = _rgb('#DA70D6') + goldenrod = _rgb('#DAA520') + palevioletred = _rgb('#DB7093') + crimson = _rgb('#DC143C') + gainsboro = _rgb('#DCDCDC') + plum = _rgb('#DDA0DD') + burlywood = _rgb('#DEB887') + lightcyan = _rgb('#E0FFFF') + lavender = _rgb('#E6E6FA') + darksalmon = _rgb('#E9967A') + violet = _rgb('#EE82EE') + palegoldenrod = _rgb('#EEE8AA') + lightcoral = _rgb('#F08080') + khaki = _rgb('#F0E68C') + aliceblue = _rgb('#F0F8FF') + honeydew = _rgb('#F0FFF0') + azure = _rgb('#F0FFFF') + sandybrown = _rgb('#F4A460') + wheat = _rgb('#F5DEB3') + beige = _rgb('#F5F5DC') + whitesmoke = _rgb('#F5F5F5') + mintcream = _rgb('#F5FFFA') + ghostwhite = _rgb('#F8F8FF') + salmon = _rgb('#FA8072') + antiquewhite = _rgb('#FAEBD7') + linen = _rgb('#FAF0E6') + lightgoldenrodyellow = _rgb('#FAFAD2') + oldlace = _rgb('#FDF5E6') + magenta = _rgb('#FF00FF') + deeppink = _rgb('#FF1493') + orangered = _rgb('#FF4500') + tomato = _rgb('#FF6347') + hotpink = _rgb('#FF69B4') + coral = _rgb('#FF7F50') + darkorange = _rgb('#FF8C00') + lightsalmon = _rgb('#FFA07A') + lightpink = _rgb('#FFB6C1') + pink = _rgb('#FFC0CB') + gold = _rgb('#FFD700') + peachpuff = _rgb('#FFDAB9') + navajowhite = _rgb('#FFDEAD') + moccasin = _rgb('#FFE4B5') + bisque = _rgb('#FFE4C4') + mistyrose = _rgb('#FFE4E1') + blanchedalmond = _rgb('#FFEBCD') + papayawhip = _rgb('#FFEFD5') + lavenderblush = _rgb('#FFF0F5') + seashell = _rgb('#FFF5EE') + cornsilk = _rgb('#FFF8DC') + lemonchiffon = _rgb('#FFFACD') + floralwhite = _rgb('#FFFAF0') + snow = _rgb('#FFFAFA') + lightyellow = _rgb('#FFFFE0') + ivory = _rgb('#FFFFF0') # --- - # Named colors. - # --- - - __colors = { - 'darkblue': _rgb('#00008B'), - 'mediumblue': _rgb('#0000CD'), - 'darkgreen': _rgb('#006400'), - 'darkcyan': _rgb('#008B8B'), - 'deepskyblue': _rgb('#00BFFF'), - 'darkturquoise': _rgb('#00CED1'), - 'mediumspringgreen': _rgb('#00FA9A'), - 'springgreen': _rgb('#00FF7F'), - 'cyan': _rgb('#00FFFF'), - 'midnightblue': _rgb('#191970'), - 'dodgerblue': _rgb('#1E90FF'), - 'lightseagreen': _rgb('#20B2AA'), - 'forestgreen': _rgb('#228B22'), - 'seagreen': _rgb('#2E8B57'), - 'darkslategray': _rgb('#2F4F4F'), - 'darkslategrey': _rgb('#2F4F4F'), - 'limegreen': _rgb('#32CD32'), - 'mediumseagreen': _rgb('#3CB371'), - 'turquoise': _rgb('#40E0D0'), - 'royalblue': _rgb('#4169E1'), - 'steelblue': _rgb('#4682B4'), - 'darkslateblue': _rgb('#483D8B'), - 'mediumturquoise': _rgb('#48D1CC'), - 'indigo': _rgb('#4B0082'), - 'darkolivegreen': _rgb('#556B2F'), - 'cadetblue': _rgb('#5F9EA0'), - 'cornflowerblue': _rgb('#6495ED'), - 'mediumaquamarine': _rgb('#66CDAA'), - 'dimgray': _rgb('#696969'), - 'dimgrey': _rgb('#696969'), - 'slateblue': _rgb('#6A5ACD'), - 'olivedrab': _rgb('#6B8E23'), - 'slategray': _rgb('#708090'), - 'slategrey': _rgb('#708090'), - 'lightslategray': _rgb('#778899'), - 'lightslategrey': _rgb('#778899'), - 'mediumslateblue': _rgb('#7B68EE'), - 'lawngreen': _rgb('#7CFC00'), - 'chartreuse': _rgb('#7FFF00'), - 'aquamarine': _rgb('#7FFFD4'), - 'grey': _rgb('#808080'), - 'skyblue': _rgb('#87CEEB'), - 'lightskyblue': _rgb('#87CEFA'), - 'blueviolet': _rgb('#8A2BE2'), - 'darkred': _rgb('#8B0000'), - 'darkmagenta': _rgb('#8B008B'), - 'saddlebrown': _rgb('#8B4513'), - 'darkseagreen': _rgb('#8FBC8F'), - 'lightgreen': _rgb('#90EE90'), - 'mediumpurple': _rgb('#9370DB'), - 'darkviolet': _rgb('#9400D3'), - 'palegreen': _rgb('#98FB98'), - 'darkorchid': _rgb('#9932CC'), - 'yellowgreen': _rgb('#9ACD32'), - 'sienna': _rgb('#A0522D'), - 'brown': _rgb('#A52A2A'), - 'darkgray': _rgb('#A9A9A9'), - 'darkgrey': _rgb('#A9A9A9'), - 'lightblue': _rgb('#ADD8E6'), - 'greenyellow': _rgb('#ADFF2F'), - 'paleturquoise': _rgb('#AFEEEE'), - 'lightsteelblue': _rgb('#B0C4DE'), - 'powderblue': _rgb('#B0E0E6'), - 'firebrick': _rgb('#B22222'), - 'darkgoldenrod': _rgb('#B8860B'), - 'mediumorchid': _rgb('#BA55D3'), - 'rosybrown': _rgb('#BC8F8F'), - 'darkkhaki': _rgb('#BDB76B'), - 'mediumvioletred': _rgb('#C71585'), - 'indianred': _rgb('#CD5C5C'), - 'peru': _rgb('#CD853F'), - 'chocolate': _rgb('#D2691E'), - 'tan': _rgb('#D2B48C'), - 'lightgray': _rgb('#D3D3D3'), - 'lightgrey': _rgb('#D3D3D3'), - 'thistle': _rgb('#D8BFD8'), - 'orchid': _rgb('#DA70D6'), - 'goldenrod': _rgb('#DAA520'), - 'palevioletred': _rgb('#DB7093'), - 'crimson': _rgb('#DC143C'), - 'gainsboro': _rgb('#DCDCDC'), - 'plum': _rgb('#DDA0DD'), - 'burlywood': _rgb('#DEB887'), - 'lightcyan': _rgb('#E0FFFF'), - 'lavender': _rgb('#E6E6FA'), - 'darksalmon': _rgb('#E9967A'), - 'violet': _rgb('#EE82EE'), - 'palegoldenrod': _rgb('#EEE8AA'), - 'lightcoral': _rgb('#F08080'), - 'khaki': _rgb('#F0E68C'), - 'aliceblue': _rgb('#F0F8FF'), - 'honeydew': _rgb('#F0FFF0'), - 'azure': _rgb('#F0FFFF'), - 'sandybrown': _rgb('#F4A460'), - 'wheat': _rgb('#F5DEB3'), - 'beige': _rgb('#F5F5DC'), - 'whitesmoke': _rgb('#F5F5F5'), - 'mintcream': _rgb('#F5FFFA'), - 'ghostwhite': _rgb('#F8F8FF'), - 'salmon': _rgb('#FA8072'), - 'antiquewhite': _rgb('#FAEBD7'), - 'linen': _rgb('#FAF0E6'), - 'lightgoldenrodyellow': _rgb('#FAFAD2'), - 'oldlace': _rgb('#FDF5E6'), - 'magenta': _rgb('#FF00FF'), - 'deeppink': _rgb('#FF1493'), - 'orangered': _rgb('#FF4500'), - 'tomato': _rgb('#FF6347'), - 'hotpink': _rgb('#FF69B4'), - 'coral': _rgb('#FF7F50'), - 'darkorange': _rgb('#FF8C00'), - 'lightsalmon': _rgb('#FFA07A'), - 'lightpink': _rgb('#FFB6C1'), - 'pink': _rgb('#FFC0CB'), - 'gold': _rgb('#FFD700'), - 'peachpuff': _rgb('#FFDAB9'), - 'navajowhite': _rgb('#FFDEAD'), - 'moccasin': _rgb('#FFE4B5'), - 'bisque': _rgb('#FFE4C4'), - 'mistyrose': _rgb('#FFE4E1'), - 'blanchedalmond': _rgb('#FFEBCD'), - 'papayawhip': _rgb('#FFEFD5'), - 'lavenderblush': _rgb('#FFF0F5'), - 'seashell': _rgb('#FFF5EE'), - 'cornsilk': _rgb('#FFF8DC'), - 'lemonchiffon': _rgb('#FFFACD'), - 'floralwhite': _rgb('#FFFAF0'), - 'snow': _rgb('#FFFAFA'), - 'lightyellow': _rgb('#FFFFE0'), - 'ivory': _rgb('#FFFFF0')} - - def _color(self, name): - try: - return self.__colors[name] - except: - return super()._color(name) - - # --- - # Utilities. + # Functions. # --- - def _hsl(self, hsla, hsl_indexes): - h, s, l, alpha = hsla - hi, si, li = hsl_indexes + def hsl(self, + h: _number | _angle, + s: _number | _percentage, + l: _number | _percentage, + alpha: _number | _percentage = _number(1.0)): try: h = h.to_hue() except ValueError as e: - raise _InvalidArgumentValueError(hi, str(e)) + raise _InvalidArgumentValueError(0, str(e)) try: s = s.to_factor() except ValueError as e: - raise _InvalidArgumentValueError(si, str(e)) + raise _InvalidArgumentValueError(1, str(e)) try: l = l.to_factor() except ValueError as e: - raise _InvalidArgumentValueError(li, str(e)) + raise _InvalidArgumentValueError(2, str(e)) try: alpha = alpha.to_factor() except ValueError as e: raise _InvalidArgumentValueError(3, str(e)) - return _Reference.color(_Color(_Color.Type.HSL, h, s, l, alpha)) + return _Reference.color(_HSLColor(h, s, l, alpha)) - # --- - # Functions. - # --- - - @_Reference.alias('rgba') - def rgb(self, r: number | percentage, - g: number | percentage = number(0), b: number | percentage = number(0), - alpha: number | percentage = number(1.0)): - return self._rgb((r, g, b, alpha), (0, 1, 2)) - - @_Reference.alias('hsla') - def hsl(self, h: number | angle, s: number | percentage, - l: number | percentage, alpha: number | percentage = number(1.0)): - return self._hsl((h, s, l, alpha), (0, 1, 2)) + rgba = _alias('rgb') + hsla = _alias('hsla') class CSS4Reference(CSS3Reference): """ Named colors and functions from `CSS Color Module Level 4 <https://drafts.csswg.org/css-color/>`_. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - number = _Reference.number - percentage = _Reference.percentage - angle = _Reference.angle - color = _Reference.color + rebeccapurple = _rgb('#663399') # --- - # Named colors. - # --- - - __colors = { - 'rebeccapurple': _rgb('#663399')} - - def _color(self, name): - try: - return self.__colors[name] - except: - return super()._color(name) - - # --- - # Utilities. + # Functions. # --- - def _hwb(self, hwba, hwb_indexes): - h, w, b, alpha = hwba - hi, wi, bi = hwb_indexes + def hwb(self, h: _number | _angle, + w: _number | _percentage = _number(0), + b: _number | _percentage = _number(0), + alpha: _number | _percentage = _number(1.0)): try: h = h.to_hue() except ValueError as e: - raise _InvalidArgumentValueError(hi, str(e)) + raise _InvalidArgumentValueError(0, str(e)) try: w = w.to_factor() except ValueError as e: - raise _InvalidArgumentValueError(wi, str(e)) + raise _InvalidArgumentValueError(1, str(e)) try: b = b.to_factor() except ValueError as e: - raise _InvalidArgumentValueError(bi, str(e)) + raise _InvalidArgumentValueError(2, str(e)) try: alpha = alpha.to_factor() except ValueError as e: raise _InvalidArgumentValueError(3, str(e)) - return _Reference.color(_Color(_Color.Type.HWB, h, w, b, alpha)) + return _Reference.color(_HWBColor(h, w, b, alpha)) - # --- - # Functions. - # --- - - @_Reference.alias('hwba') - def hwb(self, h: number | angle, w: number | percentage = number(0), - b: number | percentage = number(0), - alpha: number | percentage = number(1.0)): - return self._hwb((h, w, b, alpha), (0, 1, 2)) + def gray(self, g: _number | _percentage, + alpha: _number | _percentage = _number(1.0)): - def gray(self, g: number | percentage, - alpha: number | percentage = number(1.0)): try: g = g.to_byte() except ValueError as e: @@ -397,10 +299,11 @@ class CSS4Reference(CSS3Reference): except ValueError as e: raise _InvalidArgumentValueError(1, str(e)) - return _Reference.color(_Color(_Color.Type.RGB, g, g, g, alpha)) + g /= 255 + return _Reference.color(_RGBColor(g, g, g, alpha)) - def lab(self, l: number, a: number, b: number, - alpha: number | percentage = number(1.0)): + def lab(self, l: _number, a: _number, b: _number, + alpha: _number | _percentage = _number(1.0)): try: l = l.value @@ -418,10 +321,10 @@ class CSS4Reference(CSS3Reference): except ValueError as e: raise _InvalidArgumentValueError(3, str(e)) - return _Reference.color(_Color(_Color.Type.LAB, l, a, b, alpha)) + return _Reference.color(_LABColor(l, a, b, alpha)) - def lch(self, l: number, c: number, h: number | angle, - alpha: number | percentage = number(1.0)): + def lch(self, l: _number, c: _number, h: _number | _angle, + alpha: _number | _percentage = _number(1.0)): try: l = l.value @@ -445,6 +348,8 @@ class CSS4Reference(CSS3Reference): except ValueError as e: raise _InvalidArgumentValueError(3, str(e)) - return _Reference.color(_Color(_Color.Type.LCH, l, c, h, alpha)) + return _Reference.color(_LCHColor(l, c, h, alpha)) + + hwba = _alias('hwb') # End of file. diff --git a/thcolor/builtin/_default.py b/thcolor/builtin/_default.py index dd54a2b..ca1c066 100755 --- a/thcolor/builtin/_default.py +++ b/thcolor/builtin/_default.py @@ -1,117 +1,96 @@ #!/usr/bin/env python3 -#****************************************************************************** +#************************************************************************** # Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** -""" Named colors and function definitions. Color names are case-insensitive. - Extends the CSS references. """ +#************************************************************************** +""" Named colors and function definitions. Color names are + case-insensitive. Extends the CSS references. """ from .._color import Color as _Color from .._ref import Reference as _Reference +from .._exc import InvalidArgumentValueError as _InvalidArgumentValueError from ._css import CSS4Reference as _CSS4Reference +from ..syntax import (number as _number, percentage as _percentage, + angle as _angle, color as _color, alias as _alias) __all__ = ["DefaultReference"] + class DefaultReference(_CSS4Reference): """ Functions extending the CSS Color Module Level 4 reference. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - number = _Reference.number - percentage = _Reference.percentage - angle = _Reference.angle - color = _Reference.color - # --- - # RGB functions. + # Color functions. # --- - @_Reference.alias('rbga') - def rbg(self, r: number | percentage, - b: number | percentage = number(0), g: number | percentage = number(0), - alpha: number | percentage = number(1.0)): - return self._rgb((r, g, b, alpha), (0, 2, 1)) - - @_Reference.alias('brga') - def brg(self, b: number | percentage, - r: number | percentage = number(0), g: number | percentage = number(0), - alpha: number | percentage = number(1.0)): - return self._rgb((r, g, b, alpha), (1, 2, 0)) - - @_Reference.alias('bgra') - def bgr(self, b: number | percentage, - g: number | percentage = number(0), r: number | percentage = number(0), - alpha: number | percentage = number(1.0)): - return self._rgb((r, g, b, alpha), (2, 1, 0)) - - @_Reference.alias('gbra') - def gbr(self, g: number | percentage, - b: number | percentage = number(0), r: number | percentage = number(0), - alpha: number | percentage = number(1.0)): - return self._rgb((r, g, b, alpha), (2, 0, 1)) - - @_Reference.alias('grba') - def grb(self, g: number | percentage, - r: number | percentage = number(0), b: number | percentage = number(0), - alpha: number | percentage = number(1.0)): - return self._rgb((r, g, b, alpha), (1, 0, 2)) + @_alias('rbg', 'rbga', arg_order=(0, 2, 1)) + @_alias('brg', 'brga', arg_order=(2, 0, 1)) + @_alias('bgr', 'bgra', arg_order=(2, 1, 0)) + @_alias('gbr', 'gbra', arg_order=(1, 2, 0)) + @_alias('grb', 'grba', arg_order=(1, 0, 2)) + def rgb(self, r: _number | _percentage = _number(0), + g: _number | _percentage = _number(0), + b: _number | _percentage = _number(0), + alpha: _number | _percentage = _number(1.0)): - # --- - # HLS and HWB aliases. - # --- + return super().rgb(r, g, b, alpha) - @_Reference.alias('hlsa') - def hls(self, h: number | angle, l: number | percentage, - s: number | percentage, alpha: number | percentage = number(1.0)): - return self._hsl((h, s, l, alpha), (0, 2, 1)) + @_alias('hls', 'hlsa', arg_order=(0, 2, 1)) + def hsl(self, h: _number | _angle, s: _number | _percentage, + l: _number | _percentage, + alpha: _number | _percentage = _number(1.0)): - @_Reference.alias('hbwa') - def hbw(self, h: number | angle, b: number | percentage = number(0), - w: number | percentage = number(0), - alpha: number | percentage = number(1.0)): - return self._hwb((h, w, b, alpha), (0, 2, 1)) + return super().hsl(h, s, l, alpha) - # --- - # CMYK utilities and extensions. - # --- + @_alias('hbw', 'hbwa', arg_order=(0, 2, 1)) + def hwb(self, h: _number | _angle, + w: _number | _percentage = _number(0), + b: _number | _percentage = _number(0), + alpha: _number | _percentage = _number(1.0)): - def cmyk(self, c: number | percentage, - m: number | percentage = percentage(0), - y: number | percentage = percentage(0), - k = number | percentage(0), - alpha: number | percentage = number(1.0)): - ci, mi, yi, ki = 0, 1, 2, 3 + return super().hwb(h, w, b, alpha) + + @_alias('device-cmyk') + def cmyk(self, c: _number | _percentage, + m: _number | _percentage = _percentage(0), + y: _number | _percentage = _percentage(0), + k: _number | _percentage = _percentage(0), + alpha: _number | _percentage = _number(1.0)): try: c = c.to_factor() except ValueError as e: - raise _InvalidArgumentValueError(ci, str(e)) + raise _InvalidArgumentValueError(0, str(e)) try: m = m.to_factor() except ValueError as e: - raise _InvalidArgumentValueError(mi, str(e)) + raise _InvalidArgumentValueError(1, str(e)) try: y = y.to_factor() except ValueError as e: - raise _InvalidArgumentValueError(yi, str(e)) + raise _InvalidArgumentValueError(2, str(e)) try: k = k.to_factor() except ValueError as e: - raise _InvalidArgumentValueError(ki, str(e)) + raise _InvalidArgumentValueError(3, str(e)) try: alpha = alpha.to_factor() except ValueError as e: raise _InvalidArgumentValueError(4, str(e)) - return _Reference.color(_Color(_Color.Type.CMYK, c, m, y, k, alpha)) + return _Reference.color(_Color(_Color.Type.CMYK, c, m, y, k, + alpha)) - def xyz(self, x: number | percentage, y: number | percentage, - z: number | percentage, alpha: number | percentage = number(1.0)): + def xyz(self, x: _number | _percentage, + y: _number | _percentage, z: _number | _percentage, + alpha: _number | _percentage = _number(1.0)): try: x = x.to_factor() @@ -139,15 +118,15 @@ class DefaultReference(_CSS4Reference): # Get the RGB components of a color. # --- - def red(self, col: color) -> number: + def red(self, col: _color) -> _number: r, g, b = col.to_color().rgb() return _Reference.number(r) - def green(self, col: color) -> number: + def green(self, col: _color) -> _number: r, g, b = col.to_color().rgb() return _Reference.number(g) - def blue(self, col: color) -> number: + def blue(self, col: _color) -> _number: r, g, b = col.to_color().rgb() return _Reference.number(b) @@ -155,7 +134,7 @@ class DefaultReference(_CSS4Reference): # Manage the lightness and saturation for HSL colors. # --- - def darker(self, by: number | percentage, col: color) -> color: + def darker(self, by: _number | _percentage, col: _color) -> _color: try: by = by.to_factor() except ValueError as e: @@ -171,7 +150,7 @@ class DefaultReference(_CSS4Reference): return _Reference.color(_Color(_Color.Type.HSL, h, s, l, a)) - def lighter(self, by: number | percentage, col: color) -> color: + def lighter(self, by: _number | _percentage, col: _color) -> _color: try: by = by.to_factor() except ValueError as e: @@ -187,7 +166,7 @@ class DefaultReference(_CSS4Reference): return _Reference.color(_Color(_Color.Type.HSL, h, s, l, a)) - def desaturate(self, by: number | percentage, col: color) -> color: + def desaturate(self, by: _number | _percentage, col: _color) -> _color: try: by = by.to_factor() except ValueError as e: @@ -203,7 +182,7 @@ class DefaultReference(_CSS4Reference): return _Reference.color(_Color(_Color.Type.HSL, h, s, l, a)) - def saturate(self, by: number | percentage, col: color) -> color: + def saturate(self, by: _number | _percentage, col: _color) -> _color: try: by = by.to_factor() except ValueError as e: @@ -223,7 +202,7 @@ class DefaultReference(_CSS4Reference): # Others. # --- - def ncol(self, col: color) -> color: + def ncol(self, col: _color) -> _color: # Compatibility with w3color.js! NCols are managed directly without # the function, so the function doesn't do anything. diff --git a/thcolor/colors.py b/thcolor/colors.py new file mode 100644 index 0000000..0d0acf0 --- /dev/null +++ b/thcolor/colors.py @@ -0,0 +1,966 @@ +#!/usr/bin/env python3 +#************************************************************************** +# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# This file is part of the thcolor project, which is MIT-licensed. +#************************************************************************** +""" Color representations and conversions. """ + +from typing import Tuple as _Tuple +from collections.abc import Sequence as _Sequence, Mapping as _Mapping +from math import (ceil as _ceil, atan2 as _atan2, sqrt as _sqrt, + cos as _cos, sin as _sin, pow as _pow) + +from .angles import (Angle as _Angle, DegreesAngle as _DegreesAngle, + RadiansAngle as _RadiansAngle) + +__all__ = ['Color', + 'SRGBColor', 'HSLColor', 'HWBColor', 'CMYKColor', + 'LABColor', 'LCHColor', 'XYZColor'] + +_color_pattern = None + +class Color: + """ Class representing a color within thcolor. + + :param alpha: Value for :py:attr:`alpha`. """ + + __slots__ = ('_alpha') + _params = () + + def __init__(self, alpha: float = 1.0): + super().__init__() + + self._alpha = alpha + + def __repr__(self): + params = ((key, getattr(self, key)) + for key in self._params + ('alpha',)) + return (f"{self.__class__.__name__}(" + f"{', '.join(f'{key} = {repr(val)}' for key, val in params)})") + + @property + def alpha(self) -> float: + """ The alpha component value, varying between 0.0 (invisible) + and 1.0 (opaque). """ + + return self._alpha + + # --- + # Conversions. + # --- + + def assrgb(self) -> 'SRGBColor': + """ Get an SRGBColor out of the current object. """ + + raise NotImplementedError + + def ashsl(self) -> 'HSLColor': + """ Get an HSLColor out of the current object. """ + + return self.assrgb().ashsl() + + def ashwb(self) -> 'HWBColor': + """ Get an HWBColor out of the current object. """ + + return self.assrgb().ashwb() + + def ascmyk(self) -> 'CMYKColor': + """ Get a CMYKColor out of the current object. """ + + return self.assrgb().ascmyk() + + def aslab(self) -> 'LABColor': + """ Get a LABColor out of the current object. """ + + raise NotImplementedError + + def aslch(self) -> 'LCHColor': + """ Get a LCHColor out of the current object. """ + + raise NotImplementedError + + def asxyz(self) -> 'XYZColor': + """ Get an XYZColor out of the current object. """ + + raise NotImplementedError + + # --- + # Operations on colors. + # --- + + def replace(self, **properties: _Mapping[str, object]) -> 'Color': + """ Replace components and obtain a copy of the color. + Returns a copy of the color with the property values replaced. + + For changing the alpha on an RGB color: + + .. code-block:: python + + color = _RGBColor(1, 2, 3).replace(alpha = .5) + + For changing the lightness on an HSL color: + + .. code-block:: python + + angle = _DegreesAngle(270) + color = _HSLColor(angle, .5, 1).replace(lightness = .2) + + :param properties: Properties to change from the original + color. """ + + params = {key: getattr(self, key) for key in self._params + ('alpha',)} + + for key, value in properties.items(): + if not key in params: + raise KeyError(f"no such argument {repr(key)} in " + f"{self.__class__.__name__} parameters") + + params[key] = value + + return type(self)(**params) + + def darker(self, by: float = 0.1) -> 'HSLColor': + """ Get a darker version of the given color. + + :param by: Percentage by which the color should be darker. """ + + color = self.ashsl() + return color.replace(lightness = max(color.lightness - by, 0.0)) + + def lighter(self, by: float = 0.1) -> 'HSLColor': + """ Get a lighter version of the given color. + + :param by: Percentage by which the color should be lighter. """ + + color = self.ashsl() + return color.replace(lightness = min(color.lightness + by, 1.0)) + + def desaturate(self, by: float = 0.1) -> 'HSLColor': + """ Get a less saturated version of the given color. + + :param by: Percentage by which the color should be + desaturated. """ + + color = self.ashsl() + return color.replace(saturation = max(color.saturation - by, 0.0)) + + def saturate(self, by: float = 0.1) -> 'HSLColor': + """ Get a more saturated version of the given color. + + :param by: Percentage by which the color should be + saturated. """ + + color = self.ashsl() + return color.replace(saturation = min(color.saturation + by, 1.0)) + + # --- + # CSS utilities. + # --- + + def css(self) -> _Sequence[str]: + """ Get the CSS color descriptions, with older CSS specifications + compatibility, as a sequence of strings. + + For example: + + >>> SRGBColor(18, 52, 86, 0.82).css() + ... ("#123456", "rgba(18, 52, 86, 82%)") """ + + def _percent(prop): + per = round(prop, 4) * 100 + if per == int(per): + per = int(per) + return per + + def _deg(agl): + agl = round(agl.asdegrees().degrees, 2) + if agl == int(agl): + agl = int(agl) + return agl + + def statements(): + # Start by yelling a #RRGGBB color, compatible with most + # web browsers around the world, followed by the rgba() + # notation if the alpha value isn't 1.0. + + a = round(self.alpha, 3) + + try: + rgb = self.assrgb() + except NotImplementedError: + pass + else: + r, g, b = (int(rgb.red * 255), int(rgb.green * 255), + int(rgb.blue * 255)) + + yield f'#{r:02X}{g:02X}{b:02X}' + + if a < 1.0: + yield f'rgba({r}, {g}, {b}, {_percent(a)}%)' + + # Then yield more specific CSS declarations in case + # they're supported (which would be neat!). + + if isinstance(self, HSLColor): + hue, sat, lgt = ( + self.hue, self.saturation, self.lightness) + + args = (f'{_deg(hue)}deg, ' + f'{_percent(sat)}%, {_percent(lgt)}%') + + if a < 1.0: + yield f'hsla({args}, {_percent(a)}%)' + else: + yield f'hsl({args})' + elif isinstance(self, HWBColor): + hue, wht, blk = ( + self.hue, self.whiteness, self.blackness) + + args = f'{_deg(hue)}deg, ' \ + f'{_percent(wht)}%, {_percent(blk)}%' + + if a < 1.0: + yield f'hwba({args}, {_percent(a)}%)' + else: + yield f'hwb({args})' + + return tuple(statements()) + + # --- + # Static methods for decoding. + # --- + + @staticmethod + def from_text(expr: str) -> 'Color': + """ Create a color from a string. + + :param expr: The expression to decode. """ + + global _color_pattern + + # Load the pattern if required. + + if _color_pattern is None: + try: + import regex + except ImportError: + raise ImportError("text parsing is disabled until you " + "install the 'regex' module, e.g. via " + "'pip install regex'.") + + _color_pattern = regex.compile(r""" + ( + ((?P<agl_val>-? ([0-9]+\.?|[0-9]*\.[0-9]+)) \s* + (?P<agl_typ>deg|grad|rad|turn)) + | ((?P<per>[0-9]+(\.[0-9]*)? | \.[0-9]+) \s* \%) + | (?P<num>[0-9]+(\.[0-9]*)? | \.[0-9]+) + | (?P<ncol>[RYGCBM] [0-9]{0,2} (\.[0-9]*)?) + | (\# (?P<hex>[0-9a-f]{3} | [0-9a-f]{4} + | [0-9a-f]{6} | [0-9a-f]{8})) + | ((?P<name>[a-z]([a-z0-9\s_-]*[a-z0-9_-])?) + ( \s* \( \s* (?P<arg> (?0)? ) \s* \) )?) + ) + + \s* ((?P<sep>[,/\s])+ \s* (?P<nextargs> (?0))?)? + """, regex.VERBOSE | regex.I | regex.M) + + def recurse(column, match): + if not match: + return () + + args = recurse(column + match.start('nextargs'), + _color_pattern.fullmatch(match['nextargs'] or "")) + + value = None + if match['agl_val'] is not None: + # The matched value is an angle. + + agl_cls = { + 'deg': _DegreesAngle, + 'grad': _GradiansAngle, + 'rad': _RadiansAngle, + 'turn': _TurnsAngle + }[match['agl_typ']] + + value = _angle(agl_cls(float(match['agl_val']))) + elif match['per'] is not None: + # The matched value is a percentage. + + value = float(match['per']) + value = _percentage(value) + elif match['num'] is not None: + # The matched value is a number. + + value = _number(match['num']) + elif match['hex'] is not None: + # The matched value is a hex color. + + name = match['hex'] + + if len(name) <= 4: + name = ''.join(map(lambda x: x + x, name)) + + r = int(name[0:2], 16) + g = int(name[2:4], 16) + b = int(name[4:6], 16) + a = int(name[6:8], 16) / 255.0 if len(name) == 8 else 1.0 + + value = _color(_RGBColor.frombytes(r, g, b, a)) + elif match['arg'] is not None: + # The matched value is a function. + + name = match['name'] + args = recurse(column + match.start('arg'), + _color_pattern.fullmatch(match['arg'])) + + # Get the function and call it with the arguments. + # TODO + + raise _ColorExpressionDecodingError("not implemented", + column = column, func = name) + else: + # The match is probably a natural color (ncol), we ought + # to parse it and get the following arguments or, if + # anything is invalid, to treat it as a color name. + # + # Otherwise, it is a color name; use it directly as such. + # TODO: make ncol support not always available? + + if match['ncol'] is not None: + name = match['ncol'] + + # First, get the letter and proportion. + + letter = name[0] + number = float(name[1:]) + + if number >= 0 and number < 100: + # Get the following arguments and check. + + try: + assert len(args) >= 2 + w = args[0].value.to_factor() + b = args[1].value.to_factor() + except (AssertionError, TypeError, ValueError, + AttributeError): + w = 0 + b = 0 + else: + args = args[2:] + + # Calculate the color and return the args. + + angle_base = 'RYGCBM'.find(letter) * 60 \ + + number / 100 * 60 + color = Color(Color.Type.HWB, + _Angle(_Angle.Type.DEG, angle_base, w, b)) + + value = _color(color) + else: + name = match['name'] + + if value is None: + # The matched value is a named color. + + try: + # Get the named color (e.g. 'blue'). + + value = ref.colors[name] + assert value is not None + except AssertionError: + r, g, b = _netscape_color(name) + value = Color(Color.Type.RGB, r, g, b, 1.0) + + value = _color(value) + + return ((argument(column, value),) + args) + + # Strip the expression. + + lexpr = expr.strip() + column = (len(expr) - len(lexpr)) + expr = lexpr + del lexpr + + # Match the expression (and check it as a whole directly). + + match = _color_pattern.fullmatch(expr) + if match is None: + raise _ColorExpressionDecodingError("expression parsing " + "failed") + + # Get the result and check its type. + + results = recurse(column, match) + if len(results) > 1: + raise _ColorExpressionDecodingError("extraneous value", + column = results[1].column) + + result = results[0].value + try: + result = ref.colors[result] + except AttributeError: + raise _ColorExpressionDecodingError("expected a color", + column = column) + + return result + +# --- +# Color implementations. +# --- + +class SRGBColor(Color): + """ A color expressed using its red, green and blue channel intensities + in the sRGB profile. + + :param red: Value for :py:attr:`red`. + :param green: Value for :py:attr:`green`. + :param blue: Value for :py:attr:`blue`. + :param alpha: Value for :py:attr:`alpha`. """ + + __slots__ = ('_red', '_green', '_blue') + _params = ('red', 'green', 'blue') + + def __init__(self, red: float, green: float, blue: float, + alpha: float = 1.0): + super().__init__(alpha) + + self._red = red + self._green = green + self._blue = blue + + @property + def red(self) -> float: + """ The intensity of the red channel, between 0.0 (dark) + and 1.0 (light). """ + + return self._red + + @property + def green(self) -> float: + """ The intensity of the green channel, between 0.0 (dark) + and 1.0 (light). """ + + return self._green + + @property + def blue(self) -> float: + """ The intensity of the blue channel, between 0.0 (dark) + and 1.0 (light). """ + + return self._blue + + def assrgb(self) -> 'SRGBColor': + """ Get an SRGBColor out of the current object. """ + + return self + + def ashsl(self) -> 'HSLColor': + """ Get an HSLColor out of the current object. """ + + r, g, b = self.red, self.green, self.blue + + min_value = min((r, g, b)) + max_value = max((r, g, b)) + chroma = max_value - min_value + + if chroma == 0: + hue = 0 + elif r == max_value: + hue = (g - b) / chroma + elif g == max_value: + hue = (b - r) / chroma + 2 + else: + hue = (r - g) / chroma + 4 + + hue = hue * 60 + (hue < 0) * 360 + lgt = (min_value + max_value) / 2 + if min_value == max_value: + s = 0 + else: + s = max_value - min_value + if lgt < 0.5: + s /= max_value + min_value + else: + s /= 2 - max_value - min_value + + return _HSLColor( + hue = _DegreesAngle(round(hue, 2)), + saturation = round(s, 2), + lightness = round(lgt, 2), + alpha = self.alpha) + + def ashwb(self) -> 'HWBColor': + """ Get an HWBColor out of the current object. """ + + r, g, b = self.red, self.green, self.blue + + max_value = max((r, g, b)) + min_value = min((r, g, b)) + chroma = max_value - min_value + + if chroma == 0: + hue = 0 + elif r == max_value: + hue = (g - b) / chroma + elif g == max_value: + hue = (b - r) / chroma + 2 + elif g == max_value: + hue = (r - g) / chroma + 4 + + hue = (hue % 6) * 360 + w = min_value + b = max_value + + return _HWBColor( + hue = _DegreesAngle(hue), + whiteness = w, + blackness = b, + alpha = self.alpha) + + def ascmyk(self) -> 'CMYKColor': + """ Get a CMYKColor out of the current object. """ + + r, g, b = self.red, self.green, self.blue + + k = 1 - max((r, g, b)) + if k == 1: + c, m, y = 0, 0, 0 + else: + c = (1 - r - k) / (1 - k) + m = (1 - g - k) / (1 - k) + y = (1 - b - k) / (1 - k) + + return CMYKColor( + cyan = c, + magenta = m, + yellow = y, + black = k, + alpha = self.alpha) + + # --- + # Other utilities specific to sRGB. + # --- + + @staticmethod + def frombytes(red: int, green: int, blue: int) -> 'SRGBColor': + """ Get an sRGB color from colors using values between 0 and 255. """ + + return SRGBColor( + red = red / 255, + green = green / 255, + blue = blue / 255) + + @staticmethod + def fromnetscapecolorname(name: str) -> 'SRGBColor': + """ Get an sRGB color from a Netscape color name. """ + + name = str(name) + + # Find more about this here: https://stackoverflow.com/a/8333464 + # + # First of all: + # - we sanitize our input by replacing invalid characters + # by '0' characters (the 0xFFFF limit is due to how + # UTF-16 was managed at the time). + # - we truncate our input to 128 characters. + + name = name.lower() + name = ''.join(c if c in '0123456789abcdef' + else ('0', '00')[ord(c) > 0xFFFF] for c in name[:128])[:128] + + # Then we calculate some values we're going to need. + # `iv` is the size of the zone for a member. + # `sz` is the size of the digits slice to take in that zone + # (max. 8). + # `of` is the offset in the zone of the slice to take. + + iv = _ceil(len(name) / 3) + of = iv - 8 if iv > 8 else 0 + sz = iv - of + + # Then we isolate the slices using the values calculated + # above. `gr` will be an array of 3 or 4 digit strings + # (depending on the number of members). + + gr = list(map(lambda i: name[i * iv + of:i * iv + iv] + .ljust(sz, '0'), range(3))) + + # Check how many digits we can skip at the beginning of + # each slice. + + pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr)) + pre = min(pre, sz - 2) + + # Then we extract the values. + + it = map(lambda x: int('0' + x[pre:pre + 2], 16), gr) + r, g, b = it + + return SRGBColor.frombytes(r, g, b) + + +class HSLColor(Color): + """ A color expressed using its hue, saturation and lightness (HSL) + components. + + :param hue: Value for :py:attr:`hue`. + :param saturation: Value for :py:attr:`saturation`. + :param lightness: Value for :py:attr:`lightness`. + :param alpha: Value for :py:attr:`alpha`.""" + + __slots__ = ('_hue', '_saturation', '_lightness') + _params = ('hue', 'saturation', 'lightness') + + def __init__(self, hue: _Angle, saturation: float, lightness: float, + alpha: float = 1.0): + super().__init__(alpha) + + self._hue = hue + self._saturation = saturation + self._lightness = lightness + + @property + def hue(self) -> _Angle: + """ The hue, as an angle. """ + + return self._hue + + @property + def saturation(self) -> float: + """ The saturation, between 0.0 and 1.0. """ + + return self._saturation + + @property + def lightness(self) -> float: + """ The lightness, between 0.0 and 1.0. """ + + return self._lightness + + def assrgb(self) -> 'SRGBColor': + """ Get an SRGBColor out of the current object. """ + + lgt, hue, s = self.lightness, self.hue.asdegrees(), self.saturation + + if s == 0: + # Achromatic color. + + return lgt, lgt, lgt + + def _hue_to_rgb(t1, t2, hue): + hue %= 6 + + if hue < 1: + return t1 + (t2 - t1) * hue + elif hue < 3: + return t2 + elif hue < 4: + return t1 + (t2 - t1) * (4 - hue) + return t1 + + hue = (hue.degrees % 360) / 60 + if lgt <= 0.5: + t2 = lgt * (s + 1) + else: + t2 = lgt + s - (lgt * s) + + t1 = lgt * 2 - t2 + + return SRGBColor( + red = _hue_to_rgb(t1, t2, hue + 2), + green = _hue_to_rgb(t1, t2, hue), + blue = _hue_to_rgb(t1, t2, hue - 2), + alpha = self.alpha) + + def ashsl(self) -> 'HSLColor': + """ Get an HSLColor out of the current object. """ + + return self + + +class HWBColor(Color): + """ A color expressed using its hue, whiteness and blackness + components. + + :param hue: Value for :py:attr:`hue`. + :param whiteness: Value for :py:attr:`whiteness`. + :param blackness: Value for :py:attr:`blackness`. + :param alpha: Value for :py:attr:`alpha`. """ + + __slots__ = ('_hue', '_whiteness', '_blackness') + _params = ('hue', 'whiteness', 'blackness') + + def __init__(self, hue: _Angle, whiteness: float, blackness: float, + alpha: float = 1.0): + super().__init__(alpha) + + self._hue = hue + self._whiteness = whiteness + self._blackness = blackness + + @property + def hue(self) -> _Angle: + """ The hue, as an angle. """ + + return self._hue + + @property + def whiteness(self) -> float: + """ The whiteness, as a value between 0.0 and 1.0. """ + + return self._whiteness + + @property + def blackness(self) -> float: + """ The blackness, as a value between 0.0 and 1.0. """ + + return self._blackness + + def assrgb(self) -> 'SRGBColor': + """ Get an SRGBColor out of the current object. """ + + hue, w, bl = self.hue, self.whiteness, self.blackness + + color = HSLColor(hue, .5, 1.0).assrgb() + r, g, b = color.red, color.green, color.blue + + if w + bl > 1: + w, bl = map(lambda x: x / (w + bl), (w, bl)) + + r, g, b = map(lambda x: x * (1 - w - bl) + w, (r, g, b)) + + return SRGBColor( + red = r, + green = g, + blue = b, + alpha = self.alpha) + + def ashwb(self) -> 'HWBColor': + """ Get an HWBColor out of the current object. """ + + return self + + +class CMYKColor(Color): + """ A color expressed using its cyan, magenta, yellow and black + channel intensities. + + :param cyan: Value for :py:attr:`cyan`. + :param magenta: Value for :py:attr:`magenta`. + :param yellow: Value for :py:attr:`yellow`. + :param black: Value for :py:attr:`black`. + :param alpha: Value for :py:attr:`alpha`. """ + + __slots__ = ('_cyan', '_magenta', '_yellow', '_black') + _params = ('cyan', 'magenta', 'yellow', 'black') + + def __init__(self, cyan: float, magenta: float, yellow: float, + black: float, alpha: float = 1.0): + super().__init__(alpha) + + self._cyan = cyan + self._magenta = magenta + self._yellow = yellow + self._black = black + + @property + def cyan(self): + """ Cyan channel intensity between 0.0 and 1.0. """ + + return self._cyan + + @property + def magenta(self): + """ Magenta channel intensity between 0.0 and 1.0. """ + + return self._magenta + + @property + def yellow(self): + """ Yellow channel intensity between 0.0 and 1.0. """ + + return self._yellow + + @property + def black(self): + """ Black channel intensity between 0.0 and 1.0. """ + + return self._black + + def assrgb(self) -> 'SRGBColor': + """ Get an SRGBColor out of the current object. """ + + c, m, y, k = self.cyan, self.magenta, self.yellow, self.black + + r = 1 - min(1, c * (1 - k) + k) + g = 1 - min(1, m * (1 - k) + k) + b = 1 - min(1, y * (1 - k) + k) + + return SRGBColor( + red = r, + green = g, + blue = b, + alpha = self.alpha) + + def ascmyk(self) -> 'CMYKColor': + """ Get a CMYKColor out of the current object. """ + + return self + + +class LABColor(Color): + """ A color expressed with its lightness, and coordinates on the + A and B axises of the CIELAB colorspace. + + :param lightness: Value for :py:attr:`lightness`. + :param a: Value for :py:attr:`a`. + :param b: Value for :py:attr:`b`. + :param alpha: Value for :py:attr:`alpha`. """ + + __slots__ = ('_lightness', '_a', '_b') + _params = ('lightness', 'a', 'b') + + def __init__(self, lightness: float, a: float, b: float, + alpha: float = 1.0): + super().__init__(alpha) + + self._lightness = lightness + self._a = a + self._b = b + + @property + def lightness(self) -> float: + """ The CIE lightness (similar to the lightness + in the HSL representation) between 0.0 and 1.0. """ + + return self._lightness + + @property + def a(self) -> float: + """ The A axis value in the Lab colorspace. """ + + return self._a + + @property + def b(self) -> float: + """ The B axis value in the Lab colorspace. """ + + return self._b + + def aslab(self) -> 'LABColor': + """ Get a LABColor out of the current object. """ + + return self + + def aslch(self) -> 'LCHColor': + """ Get a LCHColor out of the current object. """ + + l, a, b = self.lightness, self.a, self.b + + return LCHColor( + lightness = l, + chroma = _sqrt(a * a + b * b), + hue = _RadiansAngle(_atan2(b, a)), + alpha = self.alpha) + + +class LCHColor(Color): + """ A color expressed using its lightness, chroma and hue within + the CIELAB color space. + + :param lightness: Value for :py:attr:`lightness`. + :param chroma: Value for :py:attr:`chroma`. + :param hue: Value for :py:attr:`hue`. + :param alpha: Value for :py:attr:`alpha`. """ + + __slots__ = ('_lightness', '_chroma', '_hue', '_alpha') + _params = ('lightness', 'chroma', 'hue') + + def __init__(self, lightness: float, chroma: float, hue: _Angle, + alpha: float = 1.0): + super().__init__(alpha) + + self._lightness = lightness + self._chroma = chroma + self._hue = hue + + @property + def lightness(self) -> float: + """ The CIE lightness (similar to the lightness + in the HSL representation) between 0.0 and 1.0. """ + + return self._lightness + + @property + def chroma(self) -> float: + """ The chroma, as a positive number theoretically + unbounded. """ + + return self._chroma + + @property + def hue(self) -> _Angle: + """ The hue, as an angle. """ + + return self._hue + + def aslab(self) -> 'LABColor': + """ Get a LABColor out of the current object. """ + + l, c, h = self.lightness, self.chroma, self.hue.asradians() + + return LABColor( + lightness = l, + a = c * _cos(h.radians), + b = c * _sin(h.radians), + alpha = self.alpha) + + def aslch(self) -> 'LCHColor': + """ Get a LCHColor out of the current object. """ + + return self + + +class XYZColor(Color): + """ A color expressed using its CIEXYZ color space coordinates. + + :param x: Value for :py:attr:`x`. + :param y: Value for :py:attr:`y`. + :param z: Value for :py:attr:`z`. + :param alpha: Value for :py:attr:`alpha`. """ + + __slots__ = ('_x', '_y', '_z') + _params = ('x', 'y', 'z') + + def __init__(self, x: float, y: float, z: float, alpha: float = 1.0): + super().__init__(alpha) + + self._x = x + self._y = y + self._z = z + + @property + def x(self) -> float: + """ The CIE X component, between 0.0 and 1.0. """ + + return self._x + + @property + def y(self) -> float: + """ The CIE Y component, between 0.0 and 1.0. """ + + return self._y + + @property + def z(self) -> float: + """ The CIE Z component, between 0.0 and 1.0. """ + + return self._z + + def asxyz(self) -> 'XYZColor': + """ Get an XYZColor out of the current object. """ + + return self + +# End of file. diff --git a/thcolor/_exc.py b/thcolor/errors.py index f4954d8..9d827b9 100755 --- a/thcolor/_exc.py +++ b/thcolor/errors.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -#****************************************************************************** -# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +#************************************************************************** +# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** -""" Exception definitions, with internal and external exceptions defined. """ +#************************************************************************** +""" Exception definitions, with internal and external exceptions + defined. """ __all__ = ["ColorExpressionDecodingError", "NotEnoughArgumentsError", "TooManyArgumentsError", @@ -16,15 +17,15 @@ __all__ = ["ColorExpressionDecodingError", class ColorExpressionDecodingError(Exception): """ A color decoding error has occurred on the text. """ - def __init__(self, text, column = None, func = None): + def __init__(self, text, column=None, func=None): try: self._column = column assert self._column >= 0 - except: + except AssertionError: self._column = None - self._func = str(func) - self._text = str(text) + self._func = str(func) if func else None + self._text = str(text) if text else "" def __str__(self): msg = "" @@ -49,8 +50,8 @@ class ColorExpressionDecodingError(Exception): @property def column(self): """ Column of the expression at which the exception has occurred, - ``None`` if the error has occurred on an unknown column or on the - whole exception. """ + ``None`` if the error has occurred on an unknown column or on + the whole exception. """ return self._column @@ -59,7 +60,8 @@ class ColorExpressionDecodingError(Exception): """ Function name we were calling when the error has occurred, either on arguments decoding or erroneous argument type or value, ``None`` if the context is unknown or the error hasn't - occurred while calling a function or decoding its arguments. """ + occurred while calling a function or decoding its + arguments. """ return self._func @@ -70,7 +72,7 @@ class ColorExpressionDecodingError(Exception): class NotEnoughArgumentsError(Exception): """ Not enough arguments. """ - def __init__(self, count, name = None): + def __init__(self, count, name=None): self._name = name self._count = count @@ -86,10 +88,11 @@ class NotEnoughArgumentsError(Exception): def count(self): return self._count + class TooManyArgumentsError(Exception): """ Too many arguments. """ - def __init__(self, count, name = None): + def __init__(self, count, name=None): self._name = name self._count = count @@ -105,10 +108,11 @@ class TooManyArgumentsError(Exception): def count(self): return self._count + class InvalidArgumentTypeError(Exception): """ Invalid argument type. """ - def __init__(self, index, expected, got, name = None): + def __init__(self, index, expected, got, name=None): self._name = name self._index = index self._expected = expected @@ -134,10 +138,11 @@ class InvalidArgumentTypeError(Exception): def got(self): return self._got + class InvalidArgumentValueError(Exception): """ Invalid argument value. """ - def __init__(self, index, text, name = None): + def __init__(self, index, text, name=None): self._name = name self._index = index self._text = text @@ -158,4 +163,5 @@ class InvalidArgumentValueError(Exception): def text(self): return self._text + # End of file. diff --git a/thcolor/reference.py b/thcolor/reference.py new file mode 100644 index 0000000..e214d3b --- /dev/null +++ b/thcolor/reference.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +#************************************************************************** +# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# This file is part of the thcolor project, which is MIT-licensed. +#************************************************************************** +""" Function and data reference. """ + +# --- +# The reference type. +# --- + +class _ReferenceType(type): + """ The reference type. """ + + def __new__(cls, clsname, superclasses, attributedict): + return super().__new__(cls, clsname, superclasses, attributedict) + +class Reference(metaclass = _ReferenceType): + """ A function and data reference. """ + + pass + +# End of file. diff --git a/thcolor/syntax/__init__.py b/thcolor/syntax/__init__.py new file mode 100644 index 0000000..2c46bbb --- /dev/null +++ b/thcolor/syntax/__init__.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +#************************************************************************** +# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# This file is part of the thcolor project, which is MIT-licensed. +#************************************************************************** +""" Syntax elements for the reference. """ + +from inspect import getfullargspec as _getfullargspec + +__all__ = ["number", "percentage", "angle", "color"] + +# --- +# Syntax elements. +# --- + +class _base_type(type): + """ The metaclass for all types used below. """ + + def __new__(mcls, name, bases, attrs): + return super().__new__(mcls, name, bases, attrs) + + def __init__(self, name, bases, attrs): + self.__name = name + + def __contains__(self, other): + return self if other == self else None + + def __or__(self, other): + return _type_or(self, other) + + def __repr__(self): + return f"<class {repr(self.__name)}>" + + def __str__(self): + return self.__name + + +class _type_or(metaclass=_base_type): + """ A type or another. """ + + def __init__(self, type1, type2): + self._type1 = type1 + self._type2 = type2 + + @property + def type1(self): + return self._type1 + + @property + def type2(self): + return self._type2 + + def __repr__(self): + return f"{repr(self._type1)} | {repr(self._type2)}" + + def __str__(self): + return f"{self._type1} or {self._type2}" + + def __or__(self, other): + return _type_or(self, other) + + def __contains__(self, other): + return other in self._type1 or other in self._type2 + + +class number(metaclass=_base_type): + """ Syntaxical element for expression decoding, representing + a number, integer or decimal, positive or negative. + + This element is usually used to represent a byte value, + a factor (usually from 0.0 to 1.0), a color or an angle + in degrees. """ + + def __init__(self, value): + if type(value) == str: + self._strvalue = value + self._value = float(value) + else: + self._value = float(value) + if self._value == int(self._value): + self._strvalue = str(int(self._value)) + else: + self._strvalue = str(value) + + def __str__(self): + return self._strvalue + + @property + def value(self): + return self._value + + def to_byte(self): + """ Make a byte (from 0 to 255) out of the number. """ + + try: + value = int(self._value) + + assert value == self._value + assert 0 <= value < 256 + except (TypeError, ValueError, AssertionError): + raise ValueError("unsuitable value for byte conversion: " + f"{repr(self._value)}") + + return value + + def to_factor(self, min=0.0, max=1.0): + """ Make a factor (usually from 0.0 to 1.0) out of the + number. """ + + if (min is not None and self._value < min) \ + or (max is not None and self._value > max): + if max is None: + msg = f"above {min}" + elif min is None: + msg = f"under {max}" + else: + msg = f"between {min} and {max}" + + raise ValueError(f"expected a value {msg}, got " + f"{self._value}") + + try: + assert 0.0 <= self._value <= 1.0 + except AssertionError: + raise ValueError("expected a value between 0.0 and 1.0, " + f"got {self._value}") + + return self._value + + def to_hue(self): + """ Make an angle in degrees out of the number. """ + + return _Angle(_Angle.Type.DEG, self._value) + +class percentage(metaclass=_base_type): + """ Syntaxical element for expression decoding, representing + a percentage (number followed by a '%' sign). + + This element is usually used to represent a factor (usually + from 0% to 100%) or anything that can be factored such as a + byte value (where 0% represents 0 and 100% represents 255). """ + + def __init__(self, value): + self._value = value + + if value < 0: + value = 0 + + # value can actually be more than 100. + + def __repr__(self): + return f"{self._value} %" + + def to_factor(self, min=0.0, max=1.0): + """ Make a factor (usually from 0.0 to 1.0) out of the + number. """ + + value = self._value / 100 + if (min is not None and value < min) \ + or (max is not None and value > max): + if max is None: + msg = f"above {min * 100}%" + elif min is None: + msg = f"under {max * 100}%" + else: + msg = f"between {min * 100}% and {max * 100}%" + + raise ValueError(f"expected a percentage {msg}, " + f"got {self._value}%") + + return value + + def to_byte(self): + """ Make a byte (from 0 to 255) out of the number. """ + + if self._value < 0: + self._value = 0 + if self._value > 100: + self._value = 100 + + return int(min(self._value / 100, 1.0) * 255) + +class angle(metaclass=_base_type): + """ Syntaxical element for expression decoding, representing + an angle (a number followed by an angle unit: degrees, + gradiants, radiants or turns). """ + + def __init__(self, value): + if not isinstance(value, _Angle): + raise TypeError("expected an Angle instance") + + self._value = value + + def __repr__(self): + return repr(self._value) + + def to_hue(self): + """ Get the :class:`thcolor.Angle` object. """ + + return self._value + +class color(metaclass=_base_type): + """ Syntaxical element for expression decoding, representing + a color using several methods: + + - a hexadecimal code preceeded by a '#', e.g. "#123ABC". + - a natural color (see + `NCol <https://www.w3schools.com/colors/colors_ncol.asp>`_). + - a color name as defined by a :class:`Reference`. + - a legacy color name using the legacy color algorithm (or + 'Netscape' color algorithm). """ + + def __init__(self, value): + if not isinstance(value, _get_color_class()): + raise ValueError("expected a Color instance") + + self._value = value + + def __repr__(self): + return repr(self._value) + + def to_color(self): + """ Get the :class:`thcolor.Color` object. """ + + return self._value + +def alias(*names, arg_order=None): + """ Decorator for aliasing with another name. Defines a lot, you + know. """ + + def _decorator(func): + nonlocal arg_order + + if not hasattr(func, '_ref_aliases'): + func._ref_aliases = () + + for name in names: + if arg_order is None: + fn = func + else: + # First of all, convert the list into a list of + # integers going from 0 onwards (instead of anything + # else that's there). + + def remake_order(l): + class alias_arg: + def __init__(self, obj): + self.obj = obj + + def __lt__(self, other): + return self.obj < other + + def __le__(self, other): + return self.obj <= other + + def __eq__(self, other): + return self.obj == other + + def __ne__(self, other): + return self.obj != other + + def __gt__(self, other): + return self.obj > other + + def __ge__(self, other): + return self.obj == other + + lst = tuple(alias_arg(o) for o in l) + srt = sorted(lst) + lst = tuple(next(i for i, m in enumerate(srt) + if m is o) for o in lst) + + return lst + + def _reorder(lst, order): + t = type(lst) + e = list(lst[len(order):]) + lst = list(lst[:len(order)]) + + for i in range(len(order) - 1, -1, -1): + e.insert(0, lst[order.index(i)]) + + return t(e) + + arg_order = remake_order(arg_order) + + # Then check if the function has that many positional + # and keyword arguments. + + args, *_ = _getfullargspec(func) + if len(arg_order) > len(args): + raise ValueError("argument order length " + f"{len(arg_order)} is more than arguments " + f"count {len(args)}") + + # Make the fnclass that serves as an + # intermediate and also give the modified function + # prototype. + + class fnclass: + def __init__(self, func, arg_order): + self._func = func + self._argo = arg_order + + def __call__(self, *args, **kwargs): + # This line supposes that the number of + # positional arguments is at least. + + args = _reorder(args, self._argo) + return self._func(*args, **kwargs) + + # Make the above functions think this is a real + # function with some reverted arguments (now that's + # the hardest part…). + + class codeclass: + def __init__(self, code): + self._code = code + + def __getattr__(self, attr): + return getattr(self._code, attr) + + fn = fnclass(func, arg_order) + co = codeclass(func.__code__) + + co.co_varnames = _reorder(co.co_varnames, arg_order) + + fn.__name__ = func.__name__ + fn.__qualname__ = func.__qualname__ + fn.__code__ = co + fn.__annotations__ = func.__annotations__ + + func._ref_aliases += ((name, fn),) + return func + + return _decorator + +# End of file. diff --git a/thcolor/version.py b/thcolor/version.py new file mode 100755 index 0000000..b51eff8 --- /dev/null +++ b/thcolor/version.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +#************************************************************************** +# Copyright (C) 2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# This file is part of the thcolor project, which is MIT-licensed. +#************************************************************************** +""" Version definition for the thcolor module. """ + +__all__ = ["version"] + +version = "0.3.1" + +# End of file. |