[doc] Ajout de la création d'un modules

[bot_base] Nouvelle gestion des modules
[config] Mise à jour de la doc
This commit is contained in:
Louis Chauvet 2020-06-12 19:28:00 +02:00
parent f6b8448cf8
commit 240d7dd417
Signed by: fomys
GPG Key ID: 1ECA046A9615ABA0
12 changed files with 311 additions and 267 deletions

133
Pipfile.lock generated
View File

@ -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"
}
}
}

View File

@ -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).

View File

@ -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

View File

@ -6,3 +6,4 @@ Module creation
:caption: Contents:
intro
basic_module

View File

@ -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 <message>": "Bot send message <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.

View File

@ -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

139
src/bot_base/modules.py Normal file
View File

@ -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__()

View File

@ -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:

View File

@ -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:

View File

@ -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.")

View File

@ -12,3 +12,7 @@ class ModuleNotFoundError(ModuleException):
class IncompatibleModuleError(ModuleException):
pass
class MissingDependency(ModuleException):
pass

View File

@ -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"))