diff --git a/Pipfile.lock b/Pipfile.lock index 37f2610..3269d33 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -31,6 +31,7 @@ "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.6.2" }, "async-timeout": { @@ -38,6 +39,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -45,6 +47,7 @@ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==19.3.0" }, "cffi": { @@ -103,43 +106,46 @@ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9" }, "multidict": { "hashes": [ - "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", - "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", - "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", - "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", - "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", - "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", - "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", - "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", - "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", - "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", - "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", - "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", - "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", - "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", - "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", - "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", - "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], - "version": "==4.7.5" + "markers": "python_version >= '3.5'", + "version": "==4.7.6" }, "packaging": { "hashes": [ - "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", - "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], "index": "pypi", - "version": "==20.3" + "version": "==20.4" }, "pycparser": { "hashes": [ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pynacl": { @@ -173,22 +179,24 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "six": { "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.14.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, "toml": { "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", - "version": "==0.10.0" + "version": "==0.10.1" }, "websockets": { "hashes": [ @@ -215,6 +223,7 @@ "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" ], + "markers": "python_full_version >= '3.6.1'", "version": "==8.1" }, "yarl": { @@ -237,6 +246,7 @@ "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" ], + "markers": "python_version >= '3.5'", "version": "==1.4.2" } }, @@ -253,6 +263,7 @@ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==19.3.0" }, "babel": { @@ -260,14 +271,15 @@ "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.0" }, "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", + "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" ], - "version": "==2020.4.5.1" + "version": "==2020.4.5.2" }, "chardet": { "hashes": [ @@ -281,6 +293,7 @@ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.16" }, "idna": { @@ -288,6 +301,7 @@ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9" }, "imagesize": { @@ -295,6 +309,7 @@ "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" }, "jinja2": { @@ -302,6 +317,7 @@ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.11.2" }, "markupsafe": { @@ -340,28 +356,31 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "more-itertools": { "hashes": [ - "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", - "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" + "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be", + "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982" ], - "version": "==8.2.0" + "markers": "python_version >= '3.5'", + "version": "==8.3.0" }, "packaging": { "hashes": [ - "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", - "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], "index": "pypi", - "version": "==20.3" + "version": "==20.4" }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "py": { @@ -369,6 +388,7 @@ "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.8.1" }, "pygments": { @@ -376,6 +396,7 @@ "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" ], + "markers": "python_version >= '3.5'", "version": "==2.6.1" }, "pyparsing": { @@ -383,36 +404,39 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "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:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", - "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" + "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1", + "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8" ], "index": "pypi", - "version": "==5.4.1" + "version": "==5.4.3" }, "pytz": { "hashes": [ - "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", - "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" + "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", + "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" ], - "version": "==2019.3" + "version": "==2020.1" }, "requests": { "hashes": [ "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.23.0" }, "six": { "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.14.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, "snowballstemmer": { "hashes": [ @@ -423,11 +447,11 @@ }, "sphinx": { "hashes": [ - "sha256:3145d87d0962366d4c5264c39094eae3f5788d01d4b1a12294051bfe4271d91b", - "sha256:d7c6e72c6aa229caf96af82f60a0d286a1521d42496c226fe37f5a75dcfe2941" + "sha256:1c445320a3310baa5ccb8d957267ef4a0fc930dc1234db5098b3d7af14fbb242", + "sha256:7d3d5087e39ab5a031b75588e9859f011de70e213cd0080ccbc28079fb0786d1" ], "index": "pypi", - "version": "==3.0.2" + "version": "==3.1.0" }, "sphinx-rtd-theme": { "hashes": [ @@ -442,6 +466,7 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -449,6 +474,7 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -456,6 +482,7 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -463,6 +490,7 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -470,6 +498,7 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -477,6 +506,7 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], + "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "urllib3": { @@ -484,6 +514,7 @@ "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", "version": "==1.25.9" }, "watchgod": { @@ -496,10 +527,10 @@ }, "wcwidth": { "hashes": [ - "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", - "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" + "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f", + "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f" ], - "version": "==0.1.9" + "version": "==0.2.4" } } } diff --git a/doc/source/index.rst b/doc/source/index.rst index 9c85fbc..825f1e7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -18,7 +18,7 @@ Introduction "Python Discord Bot" is a fully modular, self-hostable discord bot. -Its goal is to provide a solid and minimal base (only error handling, help, modules and configuration management) and to provide a large amount of modules. +Its goal is to provide a solid and minimal base (only error handling, modules and configuration management) and to provide a large amount of modules. In addition to being fully modular, this bot is meant to be a single server, in order to allow advanced configuration and simple management of private messages (many modules are games that need to use private messages, and it wouldn't be nice to add a choice of server for each action). diff --git a/doc/source/module_creation/basic_module.rst b/doc/source/module_creation/basic_module.rst new file mode 100644 index 0000000..9b20c19 --- /dev/null +++ b/doc/source/module_creation/basic_module.rst @@ -0,0 +1,34 @@ +Basic module +============ + +In last part we learn how to create a module from scratch, but as it is very long and difficult, we will use other +modules to create our first module. Theses modules are called metamodules, as they don't provide ``__main_class__`` or +``__dispatch__`` method, but only useful things for module developpers. + +Let's use ``mod_base``! + +First we need to install ``mod_base``, so follow instruction on PDMI. Then we need to add this module as dependency +to our module: + +.. code-block:: toml + :linesnos: + + version = "0.1.0" + bot_version = "~=0.2.0" + + [dependencies] + bot_base = "~=1.2.0" + +Now, mod_base is available, so we can use it in our module: + +.. code-block:: python + :linenos: + + import mod_base as m_b + + class MyModule: + def __dispatch__(self, event_name, *args, **kwargs): + pass + + __main_class__ = MyModule + diff --git a/doc/source/module_creation/index.rst b/doc/source/module_creation/index.rst index dfb87e6..66d0004 100644 --- a/doc/source/module_creation/index.rst +++ b/doc/source/module_creation/index.rst @@ -6,3 +6,4 @@ Module creation :caption: Contents: intro + basic_module \ No newline at end of file diff --git a/doc/source/module_creation/intro.rst b/doc/source/module_creation/intro.rst index 4cf956c..c450644 100644 --- a/doc/source/module_creation/intro.rst +++ b/doc/source/module_creation/intro.rst @@ -1,67 +1,49 @@ Introduction ============ -Creating a module is relatively simple: just create a python package (a folder that contains a ``__init__.py`` file) in -the modules folder, insert a ``version.json`` file (which will allow you to add dependencies and general information for -your module) and have a MainClass class in the ``__init__.py`` file. +A PDB module is a simple python package, ie. a folder which contains a ``__init__.py`` file. A valid module must have a +file named ``infos.toml`` which contains some informations about the module, and ``__init__.py__`` must have one thing: +a variable named ``__main_class__``, which point to a class, and this class must have a method named ``__dispatch__``. -So the next step is to create the :py:class:`MainClass`, which inherits from :py:class:`BaseClassPython`, here is a minimal example: +Lets look a simple example: + +.. code-block:: + + └─── modules + └─── my_module + ├── infos.toml + └── __init__.py + +Now examine ``infos.toml`` file: + +.. code-block:: toml + :linenos: + + version = "0.1.0" + bot_version = "~=0.2.0" + +This file is minimal, but necessary to describe your module, and fields are very clear: + +- ``version`` is version of module +- ``bot_version`` is the required version of bot (the ``~=`` is to say version ``0.2.0`` or compatible) + +You can refer to ``version`` section for more informations about ``version`` and ``bot_version`` fields, and +``infos file`` section for other fields of this file. + +Now look at ``__init__.py`` .. code-block:: python :linenos: - class MainClass: - name = "MyFirstModule" - help = { - "description": "My first module", - "commands": { - } - } + class MyModule: + def __dispatch__(self, event_name, *args, **kwargs): + pass -As you can see it's very simple, from now on you can start the bot and load the module. - -Currently it does nothing, so let's add a ``say`` command: - -.. code-block:: python - :linenos: - :emphasize-lines: 6,10,11 - - class MainClass: - name = "MyFirstModule" - help = { - "description": "My first module", - "commands": { - "{prefix}{command} say ": "Bot send message ", - } - } - - async def com_say(self, message, args, kwargs): - await message.channel.send(args[0]) - -You can now reload the module and test the command ``!myfirstmodule say "Hello world"``. - -You can see that without the quotation marks the returned message contains only the first word. Indeed each message is -processed to extract the module (here ``module``), the command (here ``say``) and the arguments. This is how the -arguments are processed: + __main_class__ = MyModule -``!mymodule say "Hello world" "Goodbye world"`` - ``args = ["Hello world", "Goodbye world"] kwargs=[]`` - -``!mymodule say --long-option -an -s "s value"`` - ``args = [] kwargs = [("long-option", None), ("a", None), ("n", None), ("s", "s value")]`` - -``!mymodule say -s "s value" "value"`` - ``args = ["value"] kwargs = [("s", "s value")]`` - -So let's add an ``-m`` option that adds the mention of the author to the message: - - -.. code-block:: python - :linenos: - :lineno-start: 10 - :emphasize-lines: 2,3,4 - - async def com_say(self, message, args, kwargs): - if 'm' in [k for k, v in kwargs]: - await message.channel.send(message.author.mention + args[0]) - return - await message.channel.send(args[0]) +As you can see a module is very simple. +``__dispatch__`` method will be called for each event, these events are listed in section ``events``. As you can see, +there is a lot of event types, and handle them manually will be very long, so there is a module, who parse them, and +call ``on_{event}`` method, and an other one who parse message to handle commands. In next part we learn to use them. \ No newline at end of file diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index 1b5a33d..d191a94 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -11,12 +11,12 @@ import discord import toml from packaging.specifiers import SpecifierSet, InvalidSpecifier +from bot_base.modules import ModuleManager from config import Config, config_types from config.config_types import factory import errors __version__ = "0.2.0" -MINIMAL_INFOS = ["version", "bot_version"] class BotBase(discord.Client): @@ -29,179 +29,32 @@ class BotBase(discord.Client): os.makedirs(data_folder, exist_ok=True) # Add module folder to search path # TODO: Vérifier que ca ne casse rien - sys.path.insert(0, modules_folder) # Setup logging self.log = logging.getLogger('bot_base') - # Content: {"module_name": {"module": imported module, "class": initialized class}} - self.modules = {} # Setup config self.configs = {} self.config = Config(path=os.path.join(data_folder, "config.toml")) - self.config.register("modules", factory(config_types.List, factory(config_types.Str))) self.config.register("data_folder", factory(config_types.Str)) - self.config.register("modules_folder", factory(config_types.Str)) self.config.set({ - "modules": [], "data_folder": data_folder, - "modules_folder": modules_folder, }, no_save=True) self.config.load() + self.modules = ModuleManager(self) + async def on_ready(self): self.info("Bot ready.") - try: - self.load_modules() - except errors.ModuleException as e: - self.loop.stop() - raise e - - def load_modules(self): - self.info("Load modules...") - for module in self.config["modules"]: - if module not in self.modules.keys(): - self.load_module(module) - self.info("Modules loaded.") - - def load_module(self, module: str) -> None: - """ - Try to load module - - :raise ModuleNotFoundError: If module is not in module folder - :raise IncompatibleModuleError: If module is incompatible - :param str module: module to load - """ - self.info(f"Attempt to load module {module}...") - # Check if module exists - if not os.path.isdir(os.path.join(self.config["modules_folder"], module)): - self.warning(f"Attempt to load unknown module {module}.") - raise errors.ModuleNotFoundError( - f"Module {module} not found in modules folder ({self.config['modules_folder']}.)") - if not os.path.isfile(os.path.join(self.config["modules_folder"], module, "infos.toml")): - self.warning(f"Attempt to load incompatible module {module}: no infos.toml found") - raise errors.IncompatibleModuleError(f"Module {module} is incompatible: no infos.toml found.") - # Check infos.toml integrity - with open(os.path.join(self.config["modules_folder"], module, "infos.toml")) as f: - infos = toml.load(f) - for key in MINIMAL_INFOS: - if key not in infos.keys(): - self.warning(f"Attempt to load incompatible module {module}: missing information {key}") - raise errors.IncompatibleModuleError(f"Missing information for module {module}: missing {key}.") - # Check bot_version - bot_version_specifier = SpecifierSet(infos["bot_version"]) - if __version__ not in bot_version_specifier: - self.warning(f"Attempt to load incompatible module {module}: need bot version {infos['bot_version']} " - f"and you have {__version__}") - raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current bot version " - f"(need {infos['bot_version']} and you have {__version__}).") - # Check dependencies - if infos.get("dependencies"): - for dep, version in infos["dependencies"].items(): - if not dep in self.modules.keys(): - self.load_module(dep) - dep_version_specifier = None - try: - dep_version_specifier = SpecifierSet(version) - except InvalidSpecifier: - self.warning( - f"Attempt to load incompatible module {module}: dependance version is invalid ({version} for {dep})") - raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current " - f"installation (version specifier {version} for {dep} is " - f"invalid.") - if self.modules[dep]["infos"]["version"] not in dep_version_specifier: - self.warning(f"Attempt to load incompatible module {module}: require {dep} ({version}) " - f"and you have {dep} ({self.modules[dep]['infos']['version']})") - raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current install " - f"(require {dep} ({version}) and you have {dep} " - f"({self.modules[dep]['infos']['version']})") - - # Check if module is meta - if infos.get("metamodule", False) == False: - # Check if module have __main_class__ - try: - imported = importlib.import_module(module) - except Exception as e: - self.warning(f"Attempt to load incompatible module {module}: failed import") - raise e - try: - main_class = imported.__main_class__ - except AttributeError: - self.warning(f"Attempt to load incompatible module {module}: no __main_class__ found") - raise errors.IncompatibleModuleError(f"Module {module} does not provide __main_class__.") - # Check if __main_class__ is a class - if not inspect.isclass(main_class): - self.warning(f"Attempt to load incompatible module {module}: __main_class__ is not a type") - raise errors.IncompatibleModuleError(f"Module {module} contains __main_class__ but it is not a type.") - try: - main_class = main_class(self) - except TypeError: - # Module don't need client reference - main_class = main_class() - # Check if __main_class__ have __dispatch__ attribute - try: - dispatch = main_class.__dispatch__ - except AttributeError: - self.warning(f"Attempt to load incompatible module {module}: __dispatch_ not found") - raise errors.IncompatibleModuleError( - f"Module {module} mainclass ({main_class}) does not provide __dispatch__" - f" attribute)") - # Check if __dispatch__ is function - if not inspect.isfunction(imported.__main_class__.__dispatch__): - self.warning(f"Attempt to load incompatible module {module}: __dispatch__ is not a function") - raise errors.IncompatibleModuleError( - f"Module {module} mainclass ({main_class}) provides __dispatch__, but it is " - f"not a function ({dispatch}).") - # Check if __dispatch__ can have variable positional and keyword aguments (to avoid future error on each event) - sig = inspect.signature(dispatch) - args_present, kwargs_present = False, False - for p in sig.parameters.values(): - if p.kind == p.VAR_POSITIONAL: - args_present = True - elif p.kind == p.VAR_KEYWORD: - kwargs_present = True - if not args_present: - self.warning(f"Attempt to load incompatible module {module}: __dispatch__ doesn't accept variable " - f"positional arguments") - raise errors.IncompatibleModuleError( - f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but " - f"this function doesn't accept variable positional arguments.") - if not kwargs_present: - self.warning(f"Attempt to load incompatible module {module}: __dispatch__ doesn't accept variable " - f"keywords arguments.") - raise errors.IncompatibleModuleError( - f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but " - f"this function doesn't accept variable keywords arguments.") - # Module is compatible! - # Add module to loaded modules - self.info(f"Add modules {module} to current modules.") - self.modules.update({ - module: { - "infos": infos, - "imported": imported, - "initialized_class": main_class, - "dispatch": dispatch, - } - }) - else: # Module is metamodule - self.info(f"Add modules {module} to current modules") - self.modules.update({ - module: { - "infos": infos, - "dispatch": lambda *x, **y: None - } - }) - if module not in self.config["modules"]: - self.config.set({"modules": self.config["modules"] + [module]}) - self.config.save() + self.modules.load_modules() def dispatch(self, event, *args, **kwargs): """Dispatch event""" super().dispatch(event, *args, **kwargs) - for module in self.modules.values(): - module["dispatch"](event, *args, **kwargs) + for module in self.modules: + module.dispatch(event, *args, **kwargs) async def on_error(self, event_method, *args, **kwargs): self.error(f"Error in {event_method}: \n{traceback.format_exc()}") @@ -225,6 +78,9 @@ class BotBase(discord.Client): # Configuration def get_config(self, path): + path = os.path.join(self.config["data_folder"], path) + config = self.configs.get(path) or Config(path=path) self.configs.update({ - path: self.configs.get(path) or Config(path=path) + path: config }) + return config diff --git a/src/bot_base/modules.py b/src/bot_base/modules.py new file mode 100644 index 0000000..4e783f3 --- /dev/null +++ b/src/bot_base/modules.py @@ -0,0 +1,139 @@ +import importlib +import os +import sys + +import toml +from packaging.specifiers import SpecifierSet +import errors +from config import config_types +from config.base import BaseType +from config.config_types import factory +import typing + +MINIMAL_INFOS = ["version", "bot_version"] + +__version__ = "0.2.0" + +class Dependency: + def __init__(self, name, version): + self.name = name + self.version = version + + +class Module: + def __init__(self, module_manager, name): + self.name = name + self.module_manager = module_manager + self.__infos = None + self.__path = os.path.join(self.module_manager.config["modules_folder"], name) + + self.__module = None + self.__class = None + self.__dispatch = lambda *x, **y: None + + def dispatch(self, *args, **kwargs): + return self.__dispatch(*args, **kwargs) + + @property + def version(self): + """Get version of module""" + return self.infos.get("version") + + @property + def exists(self): + """Check if module exists""" + return os.path.isdir(self.__path) + + @property + def has_infos(self): + """Check if module contains all necessary files""" + if not self.exists: + raise errors.ModuleNotFoundError(f"Module {self.name} not found here: {self.__path}.") + return os.path.isfile(os.path.join(self.__path, "infos.toml")) + + @property + def infos(self): + if not self.has_infos: + raise errors.IncompatibleModuleError(f"Module {self.name} doesn't have infos.toml.") + if not self.__infos: + with open(os.path.join(self.__path, "infos.toml")) as f: + self.__infos = toml.load(f) + return self.__infos + + @property + def has_all_infos(self): + """Check if all required infos are in infos.toml""" + for key in MINIMAL_INFOS: + if key not in self.infos.keys(): + return False + return True + + @property + def is_compatible_with_client(self): + """Check if module is compatible with bot version""" + bot_version_specifier = SpecifierSet(self.infos["bot_version"]) + return __version__ in bot_version_specifier + + @property + def is_metamodule(self): + """Check if module is metamodule""" + return self.infos.get("metamodule", False) + + @property + def deps(self): + deps = [] + for dep, version in self.infos.get("dependencies", dict()).items(): + deps.append(Dependency(dep, SpecifierSet(version))) + return deps + + def load(self): + self.__module = importlib.import_module(self.name) + if not self.is_metamodule: + try: + # Try creating instance with client + self.__class = self.__module.__main_class__(self.module_manager.client) + except TypeError: + self.__class = self.__module.__main_class__() + self.__dispatch = self.__class.__dispatch__ + + +class ModuleManager: + def __init__(self, client): + self.client = client + self.modules = dict() + self.dispatch_modules = dict() + + self.config = self.client.get_config("modules.toml") + self.config.register("modules_folder", factory(config_types.Str)) + self.config.register("enabled_modules", factory(config_types.List, factory(config_types.Str))) + + self.config.set({ + "modules_folder": os.environ.get("LOCAL_MODULES", "modules"), + "enabled_modules": [] + }, no_save=True) + self.config.load() + sys.path.insert(0, self.config["modules_folder"]) + + def load_module(self, name, version=None): + if name in self.modules.keys(): + return + new_module = Module(self, name) + if version is not None and new_module.version not in version: + raise errors.MissingDependency(f"Incompatible version for dependency {name}: {new_module.version}, require {version}.") + for dep in new_module.deps: + self.load_module(dep.name, version=dep.version) + new_module.load() + self.modules.update({name: new_module}) + if not new_module.is_metamodule: + self.dispatch_modules.update({name: new_module}) + return + if version is None and name not in self.modules["enabled_modules"]: + self.config.set({"enabled_modules": self.config["enabled_modules"] + [name]}) + self.config.save() + + def load_modules(self): + for module in self.config["enabled_modules"]: + self.load_module(module) + + def __iter__(self): + return self.dispatch_modules.values().__iter__() diff --git a/src/config/base.py b/src/config/base.py index c08245d..2bc83e2 100644 --- a/src/config/base.py +++ b/src/config/base.py @@ -103,8 +103,6 @@ class Config: >>> new_config.load() #doctest: +SKIP >>> new_config["my_parameter"] #doctest: +SKIP 3 - - :return: None """ if self.path is not None: try: diff --git a/src/config/config_types/__init__.py b/src/config/config_types/__init__.py index b4d860c..f9ff83a 100644 --- a/src/config/config_types/__init__.py +++ b/src/config/config_types/__init__.py @@ -23,7 +23,7 @@ class Meta(type): def factory(type: Type[BaseType], *args, **kwargs): """ - Create a new test ``type`` with parameters args and kwargs + Create a new ``type`` with parameters args and kwargs :Basic usage: diff --git a/src/config/config_types/str.py b/src/config/config_types/str.py index 61e73d5..b9cd267 100644 --- a/src/config/config_types/str.py +++ b/src/config/config_types/str.py @@ -49,7 +49,6 @@ class Str(BaseType): :raise ValueError: if attempt to set invalid value :param str value: Value to set - :return: None """ if not self.check_value(value): raise ValueError("Attempt to set incompatible value.") diff --git a/src/errors.py b/src/errors.py index e880d59..d4bb0e3 100644 --- a/src/errors.py +++ b/src/errors.py @@ -12,3 +12,7 @@ class ModuleNotFoundError(ModuleException): class IncompatibleModuleError(ModuleException): pass + + +class MissingDependency(ModuleException): + pass diff --git a/src/main.py b/src/main.py index ce546a5..87604e5 100644 --- a/src/main.py +++ b/src/main.py @@ -24,8 +24,8 @@ def setup_logging(default_path='data/log_config.json', default_level=logging.INF def main(): setup_logging() - client = BotBase(max_messages=500000, data_folder="datas", - modules_folder=os.environ.get("LOCAL_MODULES", "modules")) + print(os.environ.get("LOCAL_MODULES", "modules")) + client = BotBase(max_messages=500000, data_folder="datas") async def start_bot(): await client.start(os.environ.get("DISCORD_TOKEN"))