aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Touhey <thomas@touhey.fr>2021-09-11 21:09:06 +0200
committerThomas Touhey <thomas@touhey.fr>2021-09-11 21:09:06 +0200
commit5f54b6757abee47ee883d353e4aeda9c56dd6c89 (patch)
tree20bff9751f36f0bb5d4504f1961e51d033add177
parentb3edd31df8c96035d70c0216e444d6dd9951699f (diff)
Reworking thcolor.HEADmaster
-rw-r--r--.python-version2
-rwxr-xr-xMakefile2
-rw-r--r--Pipfile7
-rw-r--r--Pipfile.lock498
-rw-r--r--docs/Makefile11
-rw-r--r--docs/angles.rst12
-rw-r--r--docs/api.rst10
-rw-r--r--docs/api/angles.rst24
-rw-r--r--docs/api/colors.rst34
-rw-r--r--docs/colors.rst16
-rw-r--r--docs/conf.py18
-rw-r--r--docs/discuss.rst10
-rw-r--r--docs/discuss/color-expressions.rst53
-rw-r--r--docs/expressions.rst89
-rw-r--r--docs/index.rst19
-rw-r--r--docs/onboarding.rst12
-rw-r--r--docs/onboarding/installing.rst35
-rw-r--r--docs/onboarding/tweaking.rst16
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py8
-rwxr-xr-xthcolor/__init__.py23
-rwxr-xr-xthcolor/_angle.py295
-rwxr-xr-xthcolor/_color.py479
-rwxr-xr-xthcolor/_ref.py334
-rwxr-xr-xthcolor/_sys.py237
-rw-r--r--thcolor/angles.py205
-rwxr-xr-xthcolor/builtin/__init__.py4
-rwxr-xr-xthcolor/builtin/_css.py497
-rwxr-xr-xthcolor/builtin/_default.py129
-rw-r--r--thcolor/colors.py966
-rwxr-xr-xthcolor/errors.py (renamed from thcolor/_exc.py)36
-rw-r--r--thcolor/reference.py23
-rw-r--r--thcolor/syntax/__init__.py338
-rwxr-xr-xthcolor/version.py12
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
diff --git a/Makefile b/Makefile
index 3d840bc..be3a816 100755
--- a/Makefile
+++ b/Makefile
@@ -14,6 +14,8 @@ update:
docs:
@$(ST) build_sphinx
+check:
+ @$(PE) flake8
checkdocs:
@$(ST) checkdocs
diff --git a/Pipfile b/Pipfile
index 2b67cce..3ed277a 100644
--- a/Pipfile
+++ b/Pipfile
@@ -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.
diff --git a/setup.cfg b/setup.cfg
index 7ff488f..af49058 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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]
diff --git a/setup.py b/setup.py
index 6bcf1c6..7776190 100755
--- a/setup.py
+++ b/setup.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 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.