From 99b8028760a9d91686ed274dcecc7894095a9891 Mon Sep 17 00:00:00 2001 From: Kim Kam Date: Fri, 14 Feb 2025 21:48:44 +0200 Subject: [PATCH 01/24] docs(#1): add report template with project info and onboarding experience (#2) --- report.md | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 report.md diff --git a/report.md b/report.md new file mode 100644 index 0000000000..ad9b77147f --- /dev/null +++ b/report.md @@ -0,0 +1,108 @@ +# Report for assignment 3 + +## Project + +Name: `python-discord/bot` + +URL: https://github.com/python-discord/bot/tree/main + +It is a Discord bot specifically for use with the Python Discord server which has ~400k members. It provides numerous utilities and other tools to help keep the server running like a well-oiled machine. + +## Onboarding experience + +It mostly ran flawlessly, with a few caveats as outlined below: +- @strengthless was on Apple Silicon M2 chip, and some docker images were not pulled successfully on the first try. + - I had to refer to [this stack overflow article](https://stackoverflow.com/questions/65456814/docker-apple-silicon-m1-preview-mysql-no-matching-manifest-for-linux-arm64-v8) and run the command `export DOCKER_DEFAULT_PLATFORM=linux/x86_64/v8` +- @strengthless kept getting the `PrivilegedIntentsRequired` error upon docker startup. + - Turns out when setting up [Privileged Gateway Intents](https://www.pythondiscord.com/pages/guides/pydis-guides/contributing/bot/#privileged-intents), aside from `Server Member Intent`, you also need to enable `Message Content Intent`, which is a relatively new intent introduced by Discord. + - We have submitted a PR in [python-discord/site](https://github.com/python-discord/site/pull/1470) for updating the relevant documentations, which has been accepted. + +After setting up the environment, we ran a quick analysis on LoCs and test coverage: +```bash +# lines of code in python (~19.3k) +cloc ./bot +# branch coverage (~50%) +poetry run task test-cov && poetry run task report +``` + +With everything combined, we deemed the project suitable for this assignment. + +## Complexity + +1. What are your results for five complex functions? + * Did all methods (tools vs. manual count) get the same result? + * Are the results clear? +2. Are the functions just complex, or also long? +3. What is the purpose of the functions? +4. Are exceptions taken into account in the given measurements? +5. Is the documentation clear w.r.t. all the possible outcomes? + +## Refactoring + +Plan for refactoring complex code: + +Estimated impact of refactoring (lower CC, but other drawbacks?). + +Carried out refactoring (optional, P+): + +git diff ... + +## Coverage + +### Tools + +Document your experience in using a "new"/different coverage tool. + +How well was the tool documented? Was it possible/easy/difficult to +integrate it with your build environment? + +### Your own coverage tool + +Show a patch (or link to a branch) that shows the instrumented code to +gather coverage measurements. + +The patch is probably too long to be copied here, so please add +the git command that is used to obtain the patch instead: + +git diff ... + +What kinds of constructs does your tool support, and how accurate is +its output? + +### Evaluation + +1. How detailed is your coverage measurement? + +2. What are the limitations of your own tool? + +3. Are the results of your tool consistent with existing coverage tools? + +## Coverage improvement + +Show the comments that describe the requirements for the coverage. + +Report of old coverage: [link] + +Report of new coverage: [link] + +Test cases added: + +git diff ... + +Number of test cases added: two per team member (P) or at least four (P+). + +## Self-assessment: Way of working + +Current state according to the Essence standard: ... + +Was the self-assessment unanimous? Any doubts about certain items? + +How have you improved so far? + +Where is potential for improvement? + +## Overall experience + +What are your main take-aways from this project? What did you learn? + +Is there something special you want to mention here? From 940ff297862d55b3abafef6b750e18786843fde6 Mon Sep 17 00:00:00 2001 From: Kim Kam Date: Tue, 18 Feb 2025 00:36:49 +0200 Subject: [PATCH 02/24] docs(#5): update `refactoring` section in `report.md` for Kim (#6) --- report.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/report.md b/report.md index ad9b77147f..b737f2787a 100644 --- a/report.md +++ b/report.md @@ -40,12 +40,13 @@ With everything combined, we deemed the project suitable for this assignment. ## Refactoring Plan for refactoring complex code: +- For `humanize_delta@utils/time.py`, we plan on extracting methods, as the function is composed of two main parts, parsing of overload arguments into time delta, and stringification of the delta. Arguably, the former can be delegated to a separate helper function, which should greatly reduce the cyclomatic complexity. Estimated impact of refactoring (lower CC, but other drawbacks?). +- For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. Carried out refactoring (optional, P+): - -git diff ... +- For `humanize_delta@utils/time.py`, we have [PR #4](https://github.com/dd2480-spring-2025-group-1/bot/pull/4) which reduces CCN by 37.5%. ## Coverage From d76f12f5e8d926641dfda6906faea8a813c67957 Mon Sep 17 00:00:00 2001 From: Kim Kam Date: Tue, 18 Feb 2025 00:44:55 +0200 Subject: [PATCH 03/24] docs(#7): update `coverage improvement` section in `report.md` for Kim (#8) --- report.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/report.md b/report.md index b737f2787a..12559fbc3a 100644 --- a/report.md +++ b/report.md @@ -80,17 +80,25 @@ its output? ## Coverage improvement -Show the comments that describe the requirements for the coverage. +Show the comments that describe the requirements for the coverage: +- For `utils/helpers.py`, the functions are fairly straightforward. The requirements were already well documented in the one-line docstrings. The only caveat here is the `has_lines` function, which ignores one `\n` character from the end of the string when counting the number of lines. -Report of old coverage: [link] +Report of old coverage: +``` +Name Stmts Miss Branch BrPart Cover Missing +----------------------------------------------------------------------------------- +bot/utils/helpers.py 23 8 4 1 67% 19, 25-28, 38-43 +``` -Report of new coverage: [link] +Report of new coverage: +``` +Name Stmts Miss Branch BrPart Cover Missing +----------------------------------------------------------------------------------- +bot/utils/helpers.py 23 0 4 0 100% +``` Test cases added: - -git diff ... - -Number of test cases added: two per team member (P) or at least four (P+). +- For `utils/helpers.py`, [PR #3260](https://github.com/python-discord/bot/pull/3260) had been created by @strengthless, approved and merged into the upstream, which included 7 new test cases. ## Self-assessment: Way of working @@ -106,4 +114,4 @@ Where is potential for improvement? What are your main take-aways from this project? What did you learn? -Is there something special you want to mention here? +As an additional note for P+, we have a working patch ([PR #3260](https://github.com/python-discord/bot/pull/3260)) accepted and merged into the upstream, which included a small fix along with the addition of 7 new test cases. From bcc98500d9cb7b3b2b8fc6cc4087849ea979a8f1 Mon Sep 17 00:00:00 2001 From: Kim Kam Date: Tue, 18 Feb 2025 21:17:41 +0200 Subject: [PATCH 04/24] docs(#11): update `complexity` section in `report.md` for Kim (#12) --- report.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/report.md b/report.md index 12559fbc3a..91fafa09cd 100644 --- a/report.md +++ b/report.md @@ -31,18 +31,40 @@ With everything combined, we deemed the project suitable for this assignment. 1. What are your results for five complex functions? * Did all methods (tools vs. manual count) get the same result? + - Using [lizard](https://pypi.org/project/lizard/) (`lizard --sort nloc`), we found those five large functions: + ``` + NLOC CCN token PARAM length location + ------------------------------------------------ + 141 26 652 8 175 apply_infraction@126-300@./bot/exts/moderation/infraction/_scheduler.py + 114 17 577 5 140 deactivate_infraction@393-532@./bot/exts/moderation/infraction/_scheduler.py + 108 18 485 5 130 infraction_edit@149-278@./bot/exts/moderation/infraction/management.py + 101 16 304 5 113 humanize_delta@131-243@./bot/utils/time.py + 76 20 412 3 85 on_command_error@65-149@./bot/exts/backend/error_handler.py + ``` + - By counting manually and cross-checking, we reached the following consensus: + - For `apply_infraction@infraction/_scheduler.py`, we get + - For `deactivate_infraction@infraction/_scheduler.py`, we get + - For `infraction_edit@infraction/management.py`, we get + - For `humanize_delta@utils/time.py`, we get 13 CCN. + - For `on_command_error@backend/error_handler.py`, we get * Are the results clear? + - Some of us got different results. Upon discussing further, it was discovered that we had different methods in counting CCNs, e.g. how we deal with switch-cases, logical operators, list comprehensions, etc. Once we had those clarified, we started getting consistent results. + - The CCNs we counted were different from Lizard's. Upon taking a further look at [how Lizard works](https://github.com/terryyin/lizard/blob/master/theory.rst), it seems that Lizard is taking [logical operators](https://github.com/terryyin/lizard/issues/105) into account, while we did not. If we also take those into account, then we get the same results. 2. Are the functions just complex, or also long? + - We observe a slight correlation, but no causal effects. Generally speaking, if a function is long, then it's more probable that it contains some sort of complex code. However, there is no strict correlation here, as short functions can still be complex, vice versa. 3. What is the purpose of the functions? + - For `humanize_delta@utils/time.py`, it is a function that takes in a period of time (e.g. start and end timestamps) as its arguments, then convert it into a human-readable string. 4. Are exceptions taken into account in the given measurements? + - Yes, for both Lizard and our manual counting. If we don't take them into account, then the resultant CCN could drop. 5. Is the documentation clear w.r.t. all the possible outcomes? + - For `humanize_delta@utils/time.py`, exceptions were not explicitly documented. Other than that, the function only produces a string as its outcome, therefore we think the documentation was mostly clear. ## Refactoring Plan for refactoring complex code: - For `humanize_delta@utils/time.py`, we plan on extracting methods, as the function is composed of two main parts, parsing of overload arguments into time delta, and stringification of the delta. Arguably, the former can be delegated to a separate helper function, which should greatly reduce the cyclomatic complexity. -Estimated impact of refactoring (lower CC, but other drawbacks?). +Estimated impact of refactoring (lower CC, but other drawbacks?): - For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. Carried out refactoring (optional, P+): From 24359e67ecd63825ab63e55c6e10ee9cef4c3076 Mon Sep 17 00:00:00 2001 From: Strengthless Date: Wed, 19 Feb 2025 00:08:35 +0100 Subject: [PATCH 05/24] build(#13): update dependency `coverage` to use a forked version --- poetry.lock | 171 +++++++++++++++++++++++++++++-------------------- pyproject.toml | 2 +- 2 files changed, 103 insertions(+), 70 deletions(-) diff --git a/poetry.lock b/poetry.lock index f283334245..270df747ea 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "aiodns" @@ -6,6 +6,7 @@ version = "3.2.0" description = "Simple DNS resolver for asyncio" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5"}, {file = "aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72"}, @@ -20,6 +21,7 @@ version = "2.4.3" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, @@ -31,6 +33,7 @@ version = "3.11.11" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, @@ -128,6 +131,7 @@ version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, @@ -142,6 +146,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -153,6 +158,7 @@ version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, @@ -173,6 +179,7 @@ version = "1.3.0" description = "Better dates & times for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, @@ -192,6 +199,7 @@ version = "1.0.0rc2" description = "An easy to use asynchronous Redis cache" optional = false python-versions = "~=3.7" +groups = ["main"] files = [ {file = "async-rediscache-1.0.0rc2.tar.gz", hash = "sha256:65b1f67df0bd92defe37a3e645ea4c868da29eb41bfa493643a3b4ae7c0e109c"}, {file = "async_rediscache-1.0.0rc2-py3-none-any.whl", hash = "sha256:b156cc42b3285e1bd620487c594d7238552f95e48dc07b4e5d0b1c095c3acc86"}, @@ -209,6 +217,7 @@ version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, @@ -228,6 +237,7 @@ version = "4.12.3" description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" +groups = ["main"] files = [ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, @@ -249,6 +259,7 @@ version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, @@ -260,6 +271,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -339,6 +351,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -350,6 +363,7 @@ version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, @@ -464,10 +478,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "sys_platform == \"win32\""} [[package]] name = "coloredlogs" @@ -475,6 +491,7 @@ version = "15.0.1" description = "Colored terminal output for Python's logging module" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] files = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, @@ -488,84 +505,30 @@ cron = ["capturer (>=2.4)"] [[package]] name = "coverage" -version = "7.6.10" +version = "7.6.2a0.dev1" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.9" -files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, -] +python-versions = ">=3.8" +groups = ["dev"] +files = [] +develop = false [package.extras] toml = ["tomli"] +[package.source] +type = "git" +url = "https://github.com/devdanzin/coveragepy.git" +reference = "report_on_regions" +resolved_reference = "8e77ef56ac4e8db0bdab79db61e45d3767a7bd53" + [[package]] name = "deepdiff" version = "7.0.1" description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "deepdiff-7.0.1-py3-none-any.whl", hash = "sha256:447760081918216aa4fd4ca78a4b6a848b81307b2ea94c810255334b759e1dc3"}, {file = "deepdiff-7.0.1.tar.gz", hash = "sha256:260c16f052d4badbf60351b4f77e8390bee03a0b516246f6839bc813fb429ddf"}, @@ -584,6 +547,7 @@ version = "2.4.0" description = "A Python wrapper for the Discord API" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"}, {file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"}, @@ -604,6 +568,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -615,6 +580,7 @@ version = "2.14.0" description = "Emoji for Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "emoji-2.14.0-py3-none-any.whl", hash = "sha256:fcc936bf374b1aec67dda5303ae99710ba88cc9cdce2d1a71c5f2204e6d78799"}, {file = "emoji-2.14.0.tar.gz", hash = "sha256:f68ac28915a2221667cddb3e6c589303c3c6954c6c5af6fefaec7f9bdf72fdca"}, @@ -629,6 +595,7 @@ version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, @@ -643,6 +610,7 @@ version = "2.26.1" description = "Python implementation of redis API, can be used for testing purposes." optional = false python-versions = "<4.0,>=3.7" +groups = ["main"] files = [ {file = "fakeredis-2.26.1-py3-none-any.whl", hash = "sha256:68a5615d7ef2529094d6958677e30a6d30d544e203a5ab852985c19d7ad57e32"}, {file = "fakeredis-2.26.1.tar.gz", hash = "sha256:69f4daafe763c8014a6dbf44a17559c46643c95447b3594b3975251a171b806d"}, @@ -666,6 +634,7 @@ version = "6.0.11" description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45"}, {file = "feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5"}, @@ -680,6 +649,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -696,6 +666,7 @@ version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, @@ -797,6 +768,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -808,6 +780,7 @@ version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -829,6 +802,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -853,6 +827,7 @@ version = "10.0" description = "Human friendly output for text interfaces using Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] files = [ {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, @@ -867,6 +842,7 @@ version = "2.6.2" description = "File identification library for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, @@ -881,6 +857,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -895,6 +872,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -906,6 +884,7 @@ version = "2.2" description = "Python wrapper around Lua and LuaJIT" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "lupa-2.2-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:4bb05e3fc8f794b4a1b8a38229c3b4ae47f83cfbe7f6b172032f66d3308a0934"}, {file = "lupa-2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:13062395e716cebe25dfc6dc3738a9eb514bb052b52af25cf502c1fd74affd21"}, @@ -1001,6 +980,7 @@ version = "5.3.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, @@ -1155,6 +1135,7 @@ version = "0.14.1" description = "Convert HTML to markdown." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "markdownify-0.14.1-py3-none-any.whl", hash = "sha256:4c46a6c0c12c6005ddcd49b45a5a890398b002ef51380cd319db62df5e09bc2a"}, {file = "markdownify-0.14.1.tar.gz", hash = "sha256:a62a7a216947ed0b8dafb95b99b2ef4a0edd1e18d5653c656f68f03db2bfb2f1"}, @@ -1170,6 +1151,8 @@ version = "1.3.0" description = "shlex for windows" optional = false python-versions = ">=3.5" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4"}, {file = "mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d"}, @@ -1181,6 +1164,7 @@ version = "6.1.0" description = "multidict implementation" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, @@ -1282,6 +1266,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -1293,6 +1278,7 @@ version = "4.1.0" description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, @@ -1307,6 +1293,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1318,6 +1305,7 @@ version = "5.0.0" description = "Dump the software license list of Python packages installed with pip." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pip_licenses-5.0.0-py3-none-any.whl", hash = "sha256:82c83666753efb86d1af1c405c8ab273413eb10d6689c218df2f09acf40e477d"}, {file = "pip_licenses-5.0.0.tar.gz", hash = "sha256:0633a1f9aab58e5a6216931b0e1d5cdded8bcc2709ff563674eb0e2ff9e77e8e"}, @@ -1336,6 +1324,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1352,6 +1341,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1367,6 +1357,7 @@ version = "4.0.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, @@ -1385,6 +1376,7 @@ version = "3.12.0" description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "prettytable-3.12.0-py3-none-any.whl", hash = "sha256:77ca0ad1c435b6e363d7e8623d7cc4fcf2cf15513bf77a1c1b2e814930ac57cc"}, {file = "prettytable-3.12.0.tar.gz", hash = "sha256:f04b3e1ba35747ac86e96ec33e3bb9748ce08e254dc2a1c6253945901beec804"}, @@ -1402,6 +1394,7 @@ version = "0.2.0" description = "Accelerated property cache" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, @@ -1509,6 +1502,7 @@ version = "5.9.8" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +groups = ["dev"] files = [ {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, @@ -1537,6 +1531,7 @@ version = "4.4.0" description = "Python interface for c-ares" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6"}, {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a"}, @@ -1603,6 +1598,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1614,6 +1610,7 @@ version = "2.10.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e"}, {file = "pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560"}, @@ -1634,6 +1631,7 @@ version = "2.27.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, @@ -1746,6 +1744,7 @@ version = "2.7.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"}, {file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"}, @@ -1766,6 +1765,7 @@ version = "11.5.1" description = "PyDis core provides core functionality and utility to the bots of the Python Discord community." optional = false python-versions = "<4.0,>=3.11" +groups = ["main"] files = [ {file = "pydis_core-11.5.1-py3-none-any.whl", hash = "sha256:a4043f2b8c04f671986a86af8f58337dec1b1ac3bd4e56ac5b83e9c011b258e9"}, {file = "pydis_core-11.5.1.tar.gz", hash = "sha256:c5eb2c676cc0ac7e1557031810b150fd98af2f2014717b92a19c7420c6881478"}, @@ -1790,6 +1790,8 @@ version = "3.5.4" description = "A python implementation of GNU readline." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, @@ -1804,6 +1806,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1824,6 +1827,7 @@ version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, @@ -1842,6 +1846,7 @@ version = "0.14.1" description = "unittest subTest() support and subtests fixture" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest_subtests-0.14.1-py3-none-any.whl", hash = "sha256:e92a780d98b43118c28a16044ad9b841727bd7cb6a417073b38fd2d7ccdf052d"}, {file = "pytest_subtests-0.14.1.tar.gz", hash = "sha256:350c00adc36c3aff676a66135c81aed9e2182e15f6c3ec8721366918bbbf7580"}, @@ -1857,6 +1862,7 @@ version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, @@ -1877,6 +1883,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1891,6 +1898,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -1905,6 +1913,7 @@ version = "1.1.0" description = "Parse and manage posts with YAML (or other) frontmatter" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "python-frontmatter-1.1.0.tar.gz", hash = "sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d"}, {file = "python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1"}, @@ -1923,6 +1932,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1985,6 +1995,7 @@ version = "3.11.0" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "rapidfuzz-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb8a54543d16ab1b69e2c5ed96cabbff16db044a50eddfc028000138ca9ddf33"}, {file = "rapidfuzz-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231c8b2efbd7f8d2ecd1ae900363ba168b8870644bb8f2b5aa96e4a7573bde19"}, @@ -2085,6 +2096,7 @@ version = "4.6.0" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, @@ -2100,6 +2112,7 @@ version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, @@ -2203,6 +2216,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -2224,6 +2238,7 @@ version = "2.1.0" description = "File transport adapter for Requests" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "requests_file-2.1.0-py2.py3-none-any.whl", hash = "sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c"}, {file = "requests_file-2.1.0.tar.gz", hash = "sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658"}, @@ -2238,6 +2253,7 @@ version = "0.8.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60"}, {file = "ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac"}, @@ -2265,6 +2281,7 @@ version = "2.19.2" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"}, {file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"}, @@ -2319,6 +2336,7 @@ version = "1.0.0" description = "Py3k port of sgmllib." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, ] @@ -2329,6 +2347,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -2340,6 +2359,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2351,6 +2371,7 @@ version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, @@ -2362,6 +2383,7 @@ version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, @@ -2373,6 +2395,7 @@ version = "4.0.1" description = "A simple statsd client." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "statsd-4.0.1-py2.py3-none-any.whl", hash = "sha256:c2676519927f7afade3723aca9ca8ea986ef5b059556a980a867721ca69df093"}, {file = "statsd-4.0.1.tar.gz", hash = "sha256:99763da81bfea8daf6b3d22d11aaccb01a8d0f52ea521daab37e758a4ca7d128"}, @@ -2384,6 +2407,7 @@ version = "1.14.1" description = "tasks runner for python projects" optional = false python-versions = "<4.0,>=3.6" +groups = ["dev"] files = [ {file = "taskipy-1.14.1-py3-none-any.whl", hash = "sha256:6e361520f29a0fd2159848e953599f9c75b1d0b047461e4965069caeb94908f1"}, {file = "taskipy-1.14.1.tar.gz", hash = "sha256:410fbcf89692dfd4b9f39c2b49e1750b0a7b81affd0e2d7ea8c35f9d6a4774ed"}, @@ -2401,6 +2425,7 @@ version = "9.0.0" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, @@ -2416,6 +2441,7 @@ version = "5.1.3" description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "tldextract-5.1.3-py3-none-any.whl", hash = "sha256:78de310cc2ca018692de5ddf320f9d6bd7c5cf857d0fd4f2175f0cdf4440ea75"}, {file = "tldextract-5.1.3.tar.gz", hash = "sha256:d43c7284c23f5dc8a42fd0fee2abede2ff74cc622674e4cb07f514ab3330c338"}, @@ -2437,6 +2463,7 @@ version = "2.1.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, @@ -2448,6 +2475,7 @@ version = "2.9.0.20241003" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, @@ -2459,6 +2487,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -2470,6 +2499,7 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, @@ -2487,6 +2517,7 @@ version = "20.27.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, @@ -2507,6 +2538,7 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -2518,6 +2550,7 @@ version = "1.18.0" description = "Yet another URL library" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "yarl-1.18.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:074fee89caab89a97e18ef5f29060ef61ba3cae6cd77673acc54bfdd3214b7b7"}, {file = "yarl-1.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b026cf2c32daf48d90c0c4e406815c3f8f4cfe0c6dfccb094a9add1ff6a0e41a"}, @@ -2609,6 +2642,6 @@ multidict = ">=4.0" propcache = ">=0.2.0" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "3.12.*" -content-hash = "65543ac626a6c8c346c7eb479422ea649679eca63edd058767a2d1acd5c8ac69" +content-hash = "23eb36b78a40b4eae7df58862955577997cadf8eafbf2fcb10fbdb43fd2ed047" diff --git a/pyproject.toml b/pyproject.toml index fd7bef8577..f630bc5dd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ pydantic = "2.10.1" pydantic-settings = "2.7.1" [tool.poetry.dev-dependencies] -coverage = "7.6.10" +coverage = { git = "https://github.com/devdanzin/coveragepy.git", rev = "report_on_regions" } httpx = "0.28.1" pre-commit = "4.0.1" pip-licenses = "5.0.0" From 0a06316a5905ef93c18f9da4498c68b49726cb00 Mon Sep 17 00:00:00 2001 From: Strengthless Date: Wed, 19 Feb 2025 00:12:58 +0100 Subject: [PATCH 06/24] docs(#14): update `coverage` section in `report.md` for Kim --- report.md | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/report.md b/report.md index 91fafa09cd..65fab832d3 100644 --- a/report.md +++ b/report.md @@ -74,31 +74,46 @@ Carried out refactoring (optional, P+): ### Tools -Document your experience in using a "new"/different coverage tool. +We used [coverage.py](https://coverage.readthedocs.io/en/7.6.12/) as our main coverage tool, as the tool had already been integrated into the project. -How well was the tool documented? Was it possible/easy/difficult to -integrate it with your build environment? +The tool's usage had been well documented in `tests/README.md`, with shortcut commands implemented. + +There is only one small caveat here: coverage by function or class is not natively supported by `coverage.py` in the CLI (see this [github issue](https://github.com/nedbat/coveragepy/issues/1859) for more information). We specifically switched to a fork version of `coverage.py` for this. +- To view the branch coverage report for a specific function, you can now run `poetry run task report --functions` with the help of the fork. +- To view the missing branches, there is still no easy method. You need to add `# pragma: no cover` to ignore all other functions in the file, then export a JSON report via `poetry run coverage json ./path/to/somefile.py`. ### Your own coverage tool Show a patch (or link to a branch) that shows the instrumented code to -gather coverage measurements. - -The patch is probably too long to be copied here, so please add -the git command that is used to obtain the patch instead: - -git diff ... +gather coverage measurements: [link](https://github.com/dd2480-spring-2025-group-1/bot/pull/10) What kinds of constructs does your tool support, and how accurate is its output? +- We provide a general framework for manually appending the instrumentations: + - For boolean related constructs (e.g., `if`, `elif`, `x if y else z`, `while`, etc.), wrap the boolean with `cov_if(bool, idx, idx+1)`. + - For loop related constructs (e.g., `for`, `x for y in z`, etc.), wrap the iterable with `cov_for(list, idx, idx+1)`. + - We currently do not support switch cases, generators, or other constructs that were not explicitly mentioned above. + - You can then run `poetry run task test -rP -n 1 ./path/to/somefile.py` to run the test cases and view its coverage report. +- The reporting tool should be accurate (assuming that all required constructs are supported), though it is highly prone to human errors, as it requires manual analysis on the branches for setting up the coverage tool. ### Evaluation 1. How detailed is your coverage measurement? +- We report the overall coverage of a function, and the IDs of the branches that were not covered. +- The level of "detailness" depends on the person implementing the coverage - whether they decide to include coverage for boolean operators (i.e., `and`, `or`), exceptions, or list comprehensions etc. 2. What are the limitations of your own tool? +- As mentioned above, there are some constructs that we currently do not support. +- It requires manual analysis on the branching of the code. +- Once set up, the readability of the code is greatly affected, as a lot of `cov_if` and `cov_for` function calls are injected into the original code, which causes some degree of "obfuscation". 3. Are the results of your tool consistent with existing coverage tools? +- The reported number of total branches are consistent. +- However, the numbers of missing branches (which you can obtain using `poetry run coverage json ./bot/utils/time.py`) are different. + - Upon further investigation, we realized that it's because our tool was only checking against `test_time.py` for branch coverage on `time.py`. On the contrary, `coverage.py` records all LoC transitions when running **all** test files, then compares them against a list of possible branch transitions (statically analysed), which yields the final branch coverage report ([ref](https://coverage.readthedocs.io/en/7.6.12/branch.html#how-it-works)). + - For example, let's assume `humanize_delta@time.py` is used in the function `get_slowmode@slowmode.py`, and `get_slowmode` is tested in `test_slowmode.py`. When the test suite for `test_slowmode.py` runs, some branches within `humanize_delta@time.py` will also be executed, and thereby increasing the branch coverage on `time.py`, despite it not being tested directly. + - This explains why `coverage.py` reports higher coverage than our tool, though we would argue that no tool is in the wrong here - it's just a matter of perspective, whether you prefer to infer coverage from only the "direct" test cases, or also the "indirect" ones. + - In fact, we might even suggest that our tool is better in this case, as the indirect ones are much harder to debug if there happens to be a regression, because it's now some random test `test_slowmode.py` failing, instead of the actual culprit `humanize_delta@time.py`, which would've been reported if there were 100% "direct coverage". ## Coverage improvement From fae627fc5c68d26fbcd6afbade80a55b26f9f66c Mon Sep 17 00:00:00 2001 From: Strengthless Date: Wed, 19 Feb 2025 10:25:12 +0100 Subject: [PATCH 07/24] docs(#18): update `complexity` section in `report.md` in accordance with the latest consensus on manual counting --- report.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/report.md b/report.md index 65fab832d3..4d432be4d3 100644 --- a/report.md +++ b/report.md @@ -45,11 +45,12 @@ With everything combined, we deemed the project suitable for this assignment. - For `apply_infraction@infraction/_scheduler.py`, we get - For `deactivate_infraction@infraction/_scheduler.py`, we get - For `infraction_edit@infraction/management.py`, we get - - For `humanize_delta@utils/time.py`, we get 13 CCN. + - For `humanize_delta@utils/time.py`, we get 16 CCN. - For `on_command_error@backend/error_handler.py`, we get * Are the results clear? - Some of us got different results. Upon discussing further, it was discovered that we had different methods in counting CCNs, e.g. how we deal with switch-cases, logical operators, list comprehensions, etc. Once we had those clarified, we started getting consistent results. - - The CCNs we counted were different from Lizard's. Upon taking a further look at [how Lizard works](https://github.com/terryyin/lizard/blob/master/theory.rst), it seems that Lizard is taking [logical operators](https://github.com/terryyin/lizard/issues/105) into account, while we did not. If we also take those into account, then we get the same results. + - The CCNs we counted were mostly the same as Lizard's. There is, however, one small caveat for `apply_infraction@infraction/_scheduler.py` - we counted 27 CCN instead of 26. + - Upon further investigation, it looks like line 299 was not counted by Lizard, which included a ternary operator within a string literal. 2. Are the functions just complex, or also long? - We observe a slight correlation, but no causal effects. Generally speaking, if a function is long, then it's more probable that it contains some sort of complex code. However, there is no strict correlation here, as short functions can still be complex, vice versa. 3. What is the purpose of the functions? From 468c1605850a66f7dd995ea2b0c96da57267aded Mon Sep 17 00:00:00 2001 From: Celleforst <77944684+Celleforst@users.noreply.github.com> Date: Wed, 19 Feb 2025 22:44:43 +0100 Subject: [PATCH 08/24] docs(#23): Update `complexity` section in `report.md` with analysis of `on_command_error` function (#24) Closes issue #23 by adding complexity analysis for on_command_error function * docs(#23): add complexity analysis for on_command_error Adds complexity analysis for on_command_error function loacted in ./bot/exts/backend/error_handler.py to the report * docs(#23): Adds CCN of on_command_error function to report I forgot to add the CCN in the last commit --- report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/report.md b/report.md index 4d432be4d3..6d8e2334af 100644 --- a/report.md +++ b/report.md @@ -46,7 +46,7 @@ With everything combined, we deemed the project suitable for this assignment. - For `deactivate_infraction@infraction/_scheduler.py`, we get - For `infraction_edit@infraction/management.py`, we get - For `humanize_delta@utils/time.py`, we get 16 CCN. - - For `on_command_error@backend/error_handler.py`, we get + - For `on_command_error@backend/error_handler.py`, we get 20 CCN. * Are the results clear? - Some of us got different results. Upon discussing further, it was discovered that we had different methods in counting CCNs, e.g. how we deal with switch-cases, logical operators, list comprehensions, etc. Once we had those clarified, we started getting consistent results. - The CCNs we counted were mostly the same as Lizard's. There is, however, one small caveat for `apply_infraction@infraction/_scheduler.py` - we counted 27 CCN instead of 26. @@ -55,10 +55,12 @@ With everything combined, we deemed the project suitable for this assignment. - We observe a slight correlation, but no causal effects. Generally speaking, if a function is long, then it's more probable that it contains some sort of complex code. However, there is no strict correlation here, as short functions can still be complex, vice versa. 3. What is the purpose of the functions? - For `humanize_delta@utils/time.py`, it is a function that takes in a period of time (e.g. start and end timestamps) as its arguments, then convert it into a human-readable string. + - For `on_command_error@./bot/exts/backend/error_handler.py`, it is a function that provides error messages given a generic error by deferring errors to local error handlers. 4. Are exceptions taken into account in the given measurements? - Yes, for both Lizard and our manual counting. If we don't take them into account, then the resultant CCN could drop. 5. Is the documentation clear w.r.t. all the possible outcomes? - For `humanize_delta@utils/time.py`, exceptions were not explicitly documented. Other than that, the function only produces a string as its outcome, therefore we think the documentation was mostly clear. + - For `on_command_error@./bot/exts/backend/error_handler.py`, the documentation provides a clear and concise description of most of the functions behaviour, but seems to fail to document the `CommandInvokeError` branch behaviour almost entirely. ## Refactoring From 8ca2af200729e04cef11dd10abbb44dfaac111a2 Mon Sep 17 00:00:00 2001 From: Arvid Hjort <73788234+HerodrawzZ@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:34:10 +0100 Subject: [PATCH 09/24] doc(#31): Adds complexity analysis of apply_infraction function to the report (#34) --- report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/report.md b/report.md index 6d8e2334af..2025ab0e9f 100644 --- a/report.md +++ b/report.md @@ -42,7 +42,7 @@ With everything combined, we deemed the project suitable for this assignment. 76 20 412 3 85 on_command_error@65-149@./bot/exts/backend/error_handler.py ``` - By counting manually and cross-checking, we reached the following consensus: - - For `apply_infraction@infraction/_scheduler.py`, we get + - For `apply_infraction@infraction/_scheduler.py`, we get 27 CCN. - For `deactivate_infraction@infraction/_scheduler.py`, we get - For `infraction_edit@infraction/management.py`, we get - For `humanize_delta@utils/time.py`, we get 16 CCN. @@ -54,11 +54,13 @@ With everything combined, we deemed the project suitable for this assignment. 2. Are the functions just complex, or also long? - We observe a slight correlation, but no causal effects. Generally speaking, if a function is long, then it's more probable that it contains some sort of complex code. However, there is no strict correlation here, as short functions can still be complex, vice versa. 3. What is the purpose of the functions? + - For `apply_infraction@infraction/_scheduler.py`, it is a function that applies an infraction to the user and logs the infraction. It can also notify the user of the infraction. - For `humanize_delta@utils/time.py`, it is a function that takes in a period of time (e.g. start and end timestamps) as its arguments, then convert it into a human-readable string. - For `on_command_error@./bot/exts/backend/error_handler.py`, it is a function that provides error messages given a generic error by deferring errors to local error handlers. 4. Are exceptions taken into account in the given measurements? - Yes, for both Lizard and our manual counting. If we don't take them into account, then the resultant CCN could drop. 5. Is the documentation clear w.r.t. all the possible outcomes? + - For `apply_infraction@infraction/_scheduler.py`, the function is quite easy to read and understand by the given documentation. - For `humanize_delta@utils/time.py`, exceptions were not explicitly documented. Other than that, the function only produces a string as its outcome, therefore we think the documentation was mostly clear. - For `on_command_error@./bot/exts/backend/error_handler.py`, the documentation provides a clear and concise description of most of the functions behaviour, but seems to fail to document the `CommandInvokeError` branch behaviour almost entirely. From 509f2f2a809c57e9e8e4469366a4437b202bc62f Mon Sep 17 00:00:00 2001 From: OllanBollan <84289304+OllanBollan@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:20:21 +0100 Subject: [PATCH 10/24] Implemented test file for _scheduler and 1 test (#40) --- bot/exts/moderation/infraction/_scheduler.py | 45 ++++++++-- .../moderation/infraction/test_scheduler.py | 88 +++++++++++++++++++ 2 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 tests/bot/exts/moderation/infraction/test_scheduler.py diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 9abf3305bb..9b3958bc21 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -396,7 +396,8 @@ async def deactivate_infraction( pardon_reason: str | None = None, *, send_log: bool = True, - notify: bool = True + notify: bool = True, + flags: list[bool] = [False] * 16 ) -> dict[str, str]: """ Deactivate an active infraction and return a dictionary of lines to send in a mod log. @@ -413,6 +414,7 @@ async def deactivate_infraction( Infractions of unsupported types will raise a ValueError. """ + guild = self.bot.get_guild(constants.Guild.id) mod_role = guild.get_role(constants.Roles.moderators) user_id = infraction["user"] @@ -435,17 +437,21 @@ async def deactivate_infraction( returned_log = await self._pardon_action(infraction, notify) if returned_log is not None: + flags[0] = True log_text = {**log_text, **returned_log} # Merge the logs together else: raise ValueError( f"Attempted to deactivate an unsupported infraction #{id_} ({type_})!" ) except discord.Forbidden: + flags[1] = True log.warning(f"Failed to deactivate infraction #{id_} ({type_}): bot lacks permissions.") log_text["Failure"] = "The bot lacks permissions to do this (role hierarchy?)" log_content = mod_role.mention except discord.HTTPException as e: + flags[2] = True if e.code == 10007 or e.status == 404: + flags[3] = True log.info( f"Can't pardon {infraction['type']} for user {infraction['user']} because user left the guild." ) @@ -468,9 +474,14 @@ async def deactivate_infraction( "user__id": user_id } ) + if active_watch: + flags[4] = True + log_text["Watching"] = "Yes" - log_text["Watching"] = "Yes" if active_watch else "No" + else: + log_text["Watching"] = "No" except ResponseCodeError: + flags[5] = True log.exception(f"Failed to fetch watch status for user {user_id}") log_text["Watching"] = "Unknown - failed to fetch watch status." @@ -481,9 +492,11 @@ async def deactivate_infraction( data = {"active": False} if pardon_reason is not None: + flags[6] = True data["reason"] = "" # Append pardon reason to infraction in database. if (punish_reason := infraction["reason"]) is not None: + flags[7] = True data["reason"] = punish_reason + " | " data["reason"] += f"Pardoned: {pardon_reason}" @@ -493,30 +506,51 @@ async def deactivate_infraction( json=data ) except ResponseCodeError as e: + flags[8] = True log.exception(f"Failed to deactivate infraction #{id_} ({type_})") log_line = f"API request failed with code {e.status}." log_content = mod_role.mention # Append to an existing failure message if possible if "Failure" in log_text: + flags[9] = True log_text["Failure"] += f" {log_line}" else: log_text["Failure"] = log_line # Cancel the expiration task. if infraction["expires_at"] is not None: + flags[10] = True self.scheduler.cancel(infraction["id"]) # Send a log message to the mod log. if send_log: - log_title = "expiration failed" if "Failure" in log_text else "expired" + flags[11] = True + if "Failure" in log_text: + flags[12] = True + log_title = "expiration failed" + else: + log_title = "expired" + + #log_title = "expiration failed" if "Failure" in log_text else "expired" user = self.bot.get_user(user_id) - avatar = user.display_avatar.url if user else None + if user: + flags[13] = True + avatar = user.display_avatar.url + else: avatar = None + + #avatar = user.display_avatar.url if user else None # Move reason to end so when reason is too long, this is not gonna cut out required items. log_text["Reason"] = log_text.pop("Reason") + lines = [] + for k, v in log_text.items(): + flags[14] = True # Set your flag for each iteration + lines.append(f"{k}: {v}") + text_value = "\n".join(lines) + log.trace(f"Sending deactivation mod log for infraction #{id_}.") await send_log_message( self.bot, @@ -524,7 +558,8 @@ async def deactivate_infraction( colour=Colours.soft_green, title=f"Infraction {log_title}: {type_}", thumbnail=avatar, - text="\n".join(f"{k}: {v}" for k, v in log_text.items()), + text=text_value, + #text="\n".join(f"{k}: {v}" for k, v in log_text.items()), footer=f"ID: {id_}", content=log_content, ) diff --git a/tests/bot/exts/moderation/infraction/test_scheduler.py b/tests/bot/exts/moderation/infraction/test_scheduler.py new file mode 100644 index 0000000000..eb970daa65 --- /dev/null +++ b/tests/bot/exts/moderation/infraction/test_scheduler.py @@ -0,0 +1,88 @@ +import os +import unittest +from unittest.mock import ANY, AsyncMock, DEFAULT, MagicMock, Mock, patch +from unittest.mock import patch + +from bot.constants import Event +from bot.exts.moderation.clean import Clean +from bot.exts.moderation.infraction import _utils +from bot.exts.moderation.infraction.infractions import Infractions +from bot.exts.moderation.infraction.management import ModManagement +from bot.exts.moderation.infraction._scheduler import InfractionScheduler +from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockUser, autospec +from dotenv import load_dotenv +dotenv_path = os.path.join(os.path.dirname(__file__), "..", "bot", ".env") + +load_dotenv(dotenv_path) +from bot.log import get_logger + +class TestDeactivateInfractionMinimal(unittest.IsolatedAsyncioTestCase): + @classmethod + def setUpClass(cls): + cls.flags = [False] * 15 + + @classmethod + def tearDownClass(cls): + print(f"deactivate_infraction coverage report: {sum(cls.flags)}/15") + + def setUp(self): + self.me = MockMember(id=7890, roles=[MockRole(id=7890, position=5)]) + self.bot = MockBot() + self.cog = Infractions(self.bot) + self.user = MockMember(id=1234, roles=[MockRole(id=3577, position=10)]) + self.target = MockMember(id=1265, roles=[MockRole(id=9876, position=1)]) + self.guild = MockGuild(id=4567) + self.ctx = MockContext(me=self.me, bot=self.bot, author=self.user, guild=self.guild) + + + # Initialize the InfractionScheduler with supported infractions + self.scheduler = InfractionScheduler(self.bot, supported_infractions=["ban", "kick", "timeout", "note", "warning", "voice_mute"]) + + @patch("bot.exts.moderation.infraction._utils.INFRACTION_ICONS", {"kick": ("some_url", "some_other_url")}) + async def test_deactivate_infraction_minimal(self): + # Define a minimal infraction + infraction = { + "id": 123, + "user": 456, + "actor": 789, + "type": "kick", + "reason": "Test reason", + "inserted_at": "2023-01-01T00:00:00Z", + "expires_at": "2023-01-02T00:00:00Z", + "active": True + } + + # Mock _pardon_action to return a minimal log dictionary + self.scheduler._pardon_action = AsyncMock(return_value={}) + + # Mock the API client to succeed + self.bot.api_client.patch = AsyncMock(return_value=None) + + mock_channel = AsyncMock() + self.bot.get_channel.return_value = mock_channel + + self.cog.apply_infraction = AsyncMock() + self.bot.get_cog.return_value = AsyncMock() + self.cog.mod_log.ignore = Mock() + self.ctx.guild.ban = AsyncMock() + + # Call the method under test + log_text = await self.scheduler.deactivate_infraction(infraction, flags=self.flags) + + # Assertions to ensure the function was entered and basic behavior occurred + self.assertIn("Member", log_text) + self.assertIn("Actor", log_text) + self.assertIn("Reason", log_text) + self.assertIn("Created", log_text) + + # Ensure the infraction was marked as inactive in the database + self.bot.api_client.patch.assert_called_once_with( + f"bot/infractions/{infraction['id']}", + json={"active": False} + ) + + # Ensure _pardon_action was called + self.scheduler._pardon_action.assert_called_once_with(infraction, True) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 1f3ca89c4cd3e75abd2fa434eff44d945eb1f763 Mon Sep 17 00:00:00 2001 From: OllanBollan <84289304+OllanBollan@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:54:11 +0100 Subject: [PATCH 11/24] Revert "Implemented test file for _scheduler and 1 test (#40)" (#41) This reverts commit 509f2f2a809c57e9e8e4469366a4437b202bc62f. --- bot/exts/moderation/infraction/_scheduler.py | 45 ++-------- .../moderation/infraction/test_scheduler.py | 88 ------------------- 2 files changed, 5 insertions(+), 128 deletions(-) delete mode 100644 tests/bot/exts/moderation/infraction/test_scheduler.py diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 9b3958bc21..9abf3305bb 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -396,8 +396,7 @@ async def deactivate_infraction( pardon_reason: str | None = None, *, send_log: bool = True, - notify: bool = True, - flags: list[bool] = [False] * 16 + notify: bool = True ) -> dict[str, str]: """ Deactivate an active infraction and return a dictionary of lines to send in a mod log. @@ -414,7 +413,6 @@ async def deactivate_infraction( Infractions of unsupported types will raise a ValueError. """ - guild = self.bot.get_guild(constants.Guild.id) mod_role = guild.get_role(constants.Roles.moderators) user_id = infraction["user"] @@ -437,21 +435,17 @@ async def deactivate_infraction( returned_log = await self._pardon_action(infraction, notify) if returned_log is not None: - flags[0] = True log_text = {**log_text, **returned_log} # Merge the logs together else: raise ValueError( f"Attempted to deactivate an unsupported infraction #{id_} ({type_})!" ) except discord.Forbidden: - flags[1] = True log.warning(f"Failed to deactivate infraction #{id_} ({type_}): bot lacks permissions.") log_text["Failure"] = "The bot lacks permissions to do this (role hierarchy?)" log_content = mod_role.mention except discord.HTTPException as e: - flags[2] = True if e.code == 10007 or e.status == 404: - flags[3] = True log.info( f"Can't pardon {infraction['type']} for user {infraction['user']} because user left the guild." ) @@ -474,14 +468,9 @@ async def deactivate_infraction( "user__id": user_id } ) - if active_watch: - flags[4] = True - log_text["Watching"] = "Yes" - else: - log_text["Watching"] = "No" + log_text["Watching"] = "Yes" if active_watch else "No" except ResponseCodeError: - flags[5] = True log.exception(f"Failed to fetch watch status for user {user_id}") log_text["Watching"] = "Unknown - failed to fetch watch status." @@ -492,11 +481,9 @@ async def deactivate_infraction( data = {"active": False} if pardon_reason is not None: - flags[6] = True data["reason"] = "" # Append pardon reason to infraction in database. if (punish_reason := infraction["reason"]) is not None: - flags[7] = True data["reason"] = punish_reason + " | " data["reason"] += f"Pardoned: {pardon_reason}" @@ -506,51 +493,30 @@ async def deactivate_infraction( json=data ) except ResponseCodeError as e: - flags[8] = True log.exception(f"Failed to deactivate infraction #{id_} ({type_})") log_line = f"API request failed with code {e.status}." log_content = mod_role.mention # Append to an existing failure message if possible if "Failure" in log_text: - flags[9] = True log_text["Failure"] += f" {log_line}" else: log_text["Failure"] = log_line # Cancel the expiration task. if infraction["expires_at"] is not None: - flags[10] = True self.scheduler.cancel(infraction["id"]) # Send a log message to the mod log. if send_log: - flags[11] = True - if "Failure" in log_text: - flags[12] = True - log_title = "expiration failed" - else: - log_title = "expired" - - #log_title = "expiration failed" if "Failure" in log_text else "expired" + log_title = "expiration failed" if "Failure" in log_text else "expired" user = self.bot.get_user(user_id) - if user: - flags[13] = True - avatar = user.display_avatar.url - else: avatar = None - - #avatar = user.display_avatar.url if user else None + avatar = user.display_avatar.url if user else None # Move reason to end so when reason is too long, this is not gonna cut out required items. log_text["Reason"] = log_text.pop("Reason") - lines = [] - for k, v in log_text.items(): - flags[14] = True # Set your flag for each iteration - lines.append(f"{k}: {v}") - text_value = "\n".join(lines) - log.trace(f"Sending deactivation mod log for infraction #{id_}.") await send_log_message( self.bot, @@ -558,8 +524,7 @@ async def deactivate_infraction( colour=Colours.soft_green, title=f"Infraction {log_title}: {type_}", thumbnail=avatar, - text=text_value, - #text="\n".join(f"{k}: {v}" for k, v in log_text.items()), + text="\n".join(f"{k}: {v}" for k, v in log_text.items()), footer=f"ID: {id_}", content=log_content, ) diff --git a/tests/bot/exts/moderation/infraction/test_scheduler.py b/tests/bot/exts/moderation/infraction/test_scheduler.py deleted file mode 100644 index eb970daa65..0000000000 --- a/tests/bot/exts/moderation/infraction/test_scheduler.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import unittest -from unittest.mock import ANY, AsyncMock, DEFAULT, MagicMock, Mock, patch -from unittest.mock import patch - -from bot.constants import Event -from bot.exts.moderation.clean import Clean -from bot.exts.moderation.infraction import _utils -from bot.exts.moderation.infraction.infractions import Infractions -from bot.exts.moderation.infraction.management import ModManagement -from bot.exts.moderation.infraction._scheduler import InfractionScheduler -from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockUser, autospec -from dotenv import load_dotenv -dotenv_path = os.path.join(os.path.dirname(__file__), "..", "bot", ".env") - -load_dotenv(dotenv_path) -from bot.log import get_logger - -class TestDeactivateInfractionMinimal(unittest.IsolatedAsyncioTestCase): - @classmethod - def setUpClass(cls): - cls.flags = [False] * 15 - - @classmethod - def tearDownClass(cls): - print(f"deactivate_infraction coverage report: {sum(cls.flags)}/15") - - def setUp(self): - self.me = MockMember(id=7890, roles=[MockRole(id=7890, position=5)]) - self.bot = MockBot() - self.cog = Infractions(self.bot) - self.user = MockMember(id=1234, roles=[MockRole(id=3577, position=10)]) - self.target = MockMember(id=1265, roles=[MockRole(id=9876, position=1)]) - self.guild = MockGuild(id=4567) - self.ctx = MockContext(me=self.me, bot=self.bot, author=self.user, guild=self.guild) - - - # Initialize the InfractionScheduler with supported infractions - self.scheduler = InfractionScheduler(self.bot, supported_infractions=["ban", "kick", "timeout", "note", "warning", "voice_mute"]) - - @patch("bot.exts.moderation.infraction._utils.INFRACTION_ICONS", {"kick": ("some_url", "some_other_url")}) - async def test_deactivate_infraction_minimal(self): - # Define a minimal infraction - infraction = { - "id": 123, - "user": 456, - "actor": 789, - "type": "kick", - "reason": "Test reason", - "inserted_at": "2023-01-01T00:00:00Z", - "expires_at": "2023-01-02T00:00:00Z", - "active": True - } - - # Mock _pardon_action to return a minimal log dictionary - self.scheduler._pardon_action = AsyncMock(return_value={}) - - # Mock the API client to succeed - self.bot.api_client.patch = AsyncMock(return_value=None) - - mock_channel = AsyncMock() - self.bot.get_channel.return_value = mock_channel - - self.cog.apply_infraction = AsyncMock() - self.bot.get_cog.return_value = AsyncMock() - self.cog.mod_log.ignore = Mock() - self.ctx.guild.ban = AsyncMock() - - # Call the method under test - log_text = await self.scheduler.deactivate_infraction(infraction, flags=self.flags) - - # Assertions to ensure the function was entered and basic behavior occurred - self.assertIn("Member", log_text) - self.assertIn("Actor", log_text) - self.assertIn("Reason", log_text) - self.assertIn("Created", log_text) - - # Ensure the infraction was marked as inactive in the database - self.bot.api_client.patch.assert_called_once_with( - f"bot/infractions/{infraction['id']}", - json={"active": False} - ) - - # Ensure _pardon_action was called - self.scheduler._pardon_action.assert_called_once_with(infraction, True) - -if __name__ == "__main__": - unittest.main() \ No newline at end of file From 49d1b09a5dbd0582a4f4e912235f22fabdd82f72 Mon Sep 17 00:00:00 2001 From: Celleforst <77944684+Celleforst@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:59:05 +0100 Subject: [PATCH 12/24] docs(#25): Update `refactoring` section in `report.md` to include Marcello's refactoring (#26) * docs(#25): Update `refactoring` section in `report.md` to include refactoring in `invite.py` Adds info on refactoring of the `apply_for` function in `invite.py` * docs(#25): Add note on function change to report Adds note on function change from `on_command_error` to `apply_for` for the second part of the assignment * docs(#25): fix typo in function name in report * docs(#25): implement changes from review Implemnts changes requested by @Strengthless --- report.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/report.md b/report.md index 2025ab0e9f..0f40bf3fa7 100644 --- a/report.md +++ b/report.md @@ -68,12 +68,17 @@ With everything combined, we deemed the project suitable for this assignment. Plan for refactoring complex code: - For `humanize_delta@utils/time.py`, we plan on extracting methods, as the function is composed of two main parts, parsing of overload arguments into time delta, and stringification of the delta. Arguably, the former can be delegated to a separate helper function, which should greatly reduce the cyclomatic complexity. +- For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, we plan on extracting methods, as the function is composed of many steps that can be isolated into separate functions. One could extract functionalities like redefining invites, sorting invites, finding blocked invites and cleaning up invites into separate functions. Estimated impact of refactoring (lower CC, but other drawbacks?): -- For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. +- For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. +- For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, no drawbacks are anticipated. Carried out refactoring (optional, P+): - For `humanize_delta@utils/time.py`, we have [PR #4](https://github.com/dd2480-spring-2025-group-1/bot/pull/4) which reduces CCN by 37.5%. +- For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, we have [PR #22](https://github.com/dd2480-spring-2025-group-1/bot/pull/22) which reduces CCN by 35.1%. + +Note: since the `on_command_error` function already has 100% test coverage as reported by `coverage.py`, we decided to do part 2 of the assignment with `actions_for` instead, which is the function with the highest CCN as reported by lizard (CCN 37) and it has 20% test coverage. ## Coverage From fe2a1617c65c56a801561c96d4fe8444116e7134 Mon Sep 17 00:00:00 2001 From: OllanBollan <84289304+OllanBollan@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:10:03 +0100 Subject: [PATCH 13/24] docs(#16) Added Olivia's complexity part to report.md(#47) --- report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/report.md b/report.md index 0f40bf3fa7..e0c7a45228 100644 --- a/report.md +++ b/report.md @@ -43,7 +43,7 @@ With everything combined, we deemed the project suitable for this assignment. ``` - By counting manually and cross-checking, we reached the following consensus: - For `apply_infraction@infraction/_scheduler.py`, we get 27 CCN. - - For `deactivate_infraction@infraction/_scheduler.py`, we get + - For `deactivate_infraction@infraction/_scheduler.py`, we get 17 CCN. - For `infraction_edit@infraction/management.py`, we get - For `humanize_delta@utils/time.py`, we get 16 CCN. - For `on_command_error@backend/error_handler.py`, we get 20 CCN. @@ -54,12 +54,14 @@ With everything combined, we deemed the project suitable for this assignment. 2. Are the functions just complex, or also long? - We observe a slight correlation, but no causal effects. Generally speaking, if a function is long, then it's more probable that it contains some sort of complex code. However, there is no strict correlation here, as short functions can still be complex, vice versa. 3. What is the purpose of the functions? + - For `deactivate_infraction@infraction/_scheduler.py`, it is a function that deactivates infraction status for users in the database and returns a log of the removed infraction. - For `apply_infraction@infraction/_scheduler.py`, it is a function that applies an infraction to the user and logs the infraction. It can also notify the user of the infraction. - For `humanize_delta@utils/time.py`, it is a function that takes in a period of time (e.g. start and end timestamps) as its arguments, then convert it into a human-readable string. - For `on_command_error@./bot/exts/backend/error_handler.py`, it is a function that provides error messages given a generic error by deferring errors to local error handlers. 4. Are exceptions taken into account in the given measurements? - Yes, for both Lizard and our manual counting. If we don't take them into account, then the resultant CCN could drop. 5. Is the documentation clear w.r.t. all the possible outcomes? + - For `deactivate_infraction@infraction/_scheduler.py`, some parts of the function were easy to read with regards to all the possible outcomes, as the function utilises if/else statements in variable assignment without documenting the use case. - For `apply_infraction@infraction/_scheduler.py`, the function is quite easy to read and understand by the given documentation. - For `humanize_delta@utils/time.py`, exceptions were not explicitly documented. Other than that, the function only produces a string as its outcome, therefore we think the documentation was mostly clear. - For `on_command_error@./bot/exts/backend/error_handler.py`, the documentation provides a clear and concise description of most of the functions behaviour, but seems to fail to document the `CommandInvokeError` branch behaviour almost entirely. From a23277d34b60dd5904bd52f867d01f8a06865d8a Mon Sep 17 00:00:00 2001 From: Johan <104634655+joel90688@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:30:29 +0100 Subject: [PATCH 14/24] docs(#39): updated `report.md` for `infraction_edit` (#49) * docs(#39): added all questions for infraction_edit * docs: update README.md with consistency changes --------- Co-authored-by: Johan Nilsson Co-authored-by: Kim Kam --- report.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/report.md b/report.md index e0c7a45228..485af3c2ad 100644 --- a/report.md +++ b/report.md @@ -44,7 +44,7 @@ With everything combined, we deemed the project suitable for this assignment. - By counting manually and cross-checking, we reached the following consensus: - For `apply_infraction@infraction/_scheduler.py`, we get 27 CCN. - For `deactivate_infraction@infraction/_scheduler.py`, we get 17 CCN. - - For `infraction_edit@infraction/management.py`, we get + - For `infraction_edit@infraction/management.py`, we get 18 CCN. - For `humanize_delta@utils/time.py`, we get 16 CCN. - For `on_command_error@backend/error_handler.py`, we get 20 CCN. * Are the results clear? @@ -56,13 +56,15 @@ With everything combined, we deemed the project suitable for this assignment. 3. What is the purpose of the functions? - For `deactivate_infraction@infraction/_scheduler.py`, it is a function that deactivates infraction status for users in the database and returns a log of the removed infraction. - For `apply_infraction@infraction/_scheduler.py`, it is a function that applies an infraction to the user and logs the infraction. It can also notify the user of the infraction. + - For `infraction_edit@infraction/management.py` modifies punishments for users who have violated server rules and notifies moderators about the changes. It is used by `infraction_append`, which applies new punishments, since the two functions share most logic. The function must handle various edge cases, such as preventing edits to expired infractions or warnings. It also processes multiple input formats and validates the request before making API calls. These requirements introduce multiple decision points, contributing to its high CC. - For `humanize_delta@utils/time.py`, it is a function that takes in a period of time (e.g. start and end timestamps) as its arguments, then convert it into a human-readable string. - For `on_command_error@./bot/exts/backend/error_handler.py`, it is a function that provides error messages given a generic error by deferring errors to local error handlers. 4. Are exceptions taken into account in the given measurements? - Yes, for both Lizard and our manual counting. If we don't take them into account, then the resultant CCN could drop. 5. Is the documentation clear w.r.t. all the possible outcomes? - For `deactivate_infraction@infraction/_scheduler.py`, some parts of the function were easy to read with regards to all the possible outcomes, as the function utilises if/else statements in variable assignment without documenting the use case. - - For `apply_infraction@infraction/_scheduler.py`, the function is quite easy to read and understand by the given documentation. + - For `apply_infraction@infraction/_scheduler.py`, the function is quite easy to read and understand by the given documentation. + - For `infraction_edit@infraction/management.py` was fairly well-documented with many branches having comments describing the consequences or reasons for the branch. Additionally, it includes logging strings that serve both as messages and as documentation further describing outcomes.The documentation is overall very clear and not overstated as Python’s readability allows much to be inferred directly from the clauses. - For `humanize_delta@utils/time.py`, exceptions were not explicitly documented. Other than that, the function only produces a string as its outcome, therefore we think the documentation was mostly clear. - For `on_command_error@./bot/exts/backend/error_handler.py`, the documentation provides a clear and concise description of most of the functions behaviour, but seems to fail to document the `CommandInvokeError` branch behaviour almost entirely. @@ -71,16 +73,19 @@ With everything combined, we deemed the project suitable for this assignment. Plan for refactoring complex code: - For `humanize_delta@utils/time.py`, we plan on extracting methods, as the function is composed of two main parts, parsing of overload arguments into time delta, and stringification of the delta. Arguably, the former can be delegated to a separate helper function, which should greatly reduce the cyclomatic complexity. - For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, we plan on extracting methods, as the function is composed of many steps that can be isolated into separate functions. One could extract functionalities like redefining invites, sorting invites, finding blocked invites and cleaning up invites into separate functions. +- For `infraction_edit@infraction/management.py`, despite all code being relevant, we can still make changes to the complexity by refactoring and dividing the function into less complex functions which can be used by other functions in the future. More specifically, we can separate the rescheduling functionality from infraction_edit making a helper function which reschedules an infraction when necessary. Estimated impact of refactoring (lower CC, but other drawbacks?): - For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. -- For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, no drawbacks are anticipated. +- For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, no drawbacks are anticipated. +- For `infraction_edit@infraction/management.py`, it should decrease the CC from 18 to 11, which is a decrease of about 39%. No drawbacks are anticipated. Carried out refactoring (optional, P+): - For `humanize_delta@utils/time.py`, we have [PR #4](https://github.com/dd2480-spring-2025-group-1/bot/pull/4) which reduces CCN by 37.5%. - For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, we have [PR #22](https://github.com/dd2480-spring-2025-group-1/bot/pull/22) which reduces CCN by 35.1%. +- For `infraction_edit@infraction/management.py`, we have [PR #21](https://github.com/dd2480-spring-2025-group-1/bot/pull/21) which reduces CCN by about 39%. -Note: since the `on_command_error` function already has 100% test coverage as reported by `coverage.py`, we decided to do part 2 of the assignment with `actions_for` instead, which is the function with the highest CCN as reported by lizard (CCN 37) and it has 20% test coverage. +Note: since `on_command_error@error_handler.py` already has 100% test coverage as reported by `coverage.py`, we decided to do part 2 of the assignment with `actions_for@invite.py` instead, which is the function with the highest CCN as reported by lizard (CCN 37) and it has 20% test coverage. ## Coverage @@ -97,7 +102,9 @@ There is only one small caveat here: coverage by function or class is not native ### Your own coverage tool Show a patch (or link to a branch) that shows the instrumented code to -gather coverage measurements: [link](https://github.com/dd2480-spring-2025-group-1/bot/pull/10) +gather coverage measurements: +- For `humanize_delta@utils/time.py`, we have [PR #10](https://github.com/dd2480-spring-2025-group-1/bot/pull/10) +- For `infraction_edit@infraction/management.py`: we have [PR #28](https://github.com/dd2480-spring-2025-group-1/bot/pull/28) What kinds of constructs does your tool support, and how accurate is its output? @@ -118,6 +125,7 @@ its output? - As mentioned above, there are some constructs that we currently do not support. - It requires manual analysis on the branching of the code. - Once set up, the readability of the code is greatly affected, as a lot of `cov_if` and `cov_for` function calls are injected into the original code, which causes some degree of "obfuscation". +- Another limitation is that there will be no coverage output if no tests exist for the function since the coverage tool will never be run. 3. Are the results of your tool consistent with existing coverage tools? - The reported number of total branches are consistent. @@ -131,12 +139,18 @@ its output? Show the comments that describe the requirements for the coverage: - For `utils/helpers.py`, the functions are fairly straightforward. The requirements were already well documented in the one-line docstrings. The only caveat here is the `has_lines` function, which ignores one `\n` character from the end of the string when counting the number of lines. +- For `infraction_edit@infraction/management.py`, there were no tests before but the documentation of the function was clear and helped in creating the requirements, e.g.: + - The function should raise a BadArgument when a duration and a reason is not provided. + - The function should not allow editing the duration of a warning or note infraction. + - The function should not allow editing the duration of an expired infraction. + - The function should call the `api_client.patch` method to update an infraction when a new reason is provided. Report of old coverage: ``` Name Stmts Miss Branch BrPart Cover Missing ----------------------------------------------------------------------------------- bot/utils/helpers.py 23 8 4 1 67% 19, 25-28, 38-43 +infraction_edit@management.py 51 51 26 0 0% 192-281 ``` Report of new coverage: @@ -144,10 +158,12 @@ Report of new coverage: Name Stmts Miss Branch BrPart Cover Missing ----------------------------------------------------------------------------------- bot/utils/helpers.py 23 0 4 0 100% +infraction_edit@management.py 51 22 26 7 52% 188, 194-195, 197-202, 214, 229-242, 254-255, 261 ``` Test cases added: - For `utils/helpers.py`, [PR #3260](https://github.com/python-discord/bot/pull/3260) had been created by @strengthless, approved and merged into the upstream, which included 7 new test cases. +- For `infraction_edit@infraction/management.py`, [PR #38](https://github.com/dd2480-spring-2025-group-1/bot/pull/38) has been drafted. ## Self-assessment: Way of working From 5e8ab29e6de1bc3b517cb1115032310df4715a91 Mon Sep 17 00:00:00 2001 From: OllanBollan <84289304+OllanBollan@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:41:16 +0100 Subject: [PATCH 15/24] docs(#30) updated report with refactoring for deactivate_infraction (#48) --- report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/report.md b/report.md index 485af3c2ad..f41c1ea6a8 100644 --- a/report.md +++ b/report.md @@ -71,12 +71,14 @@ With everything combined, we deemed the project suitable for this assignment. ## Refactoring Plan for refactoring complex code: +- For `deactivate_infraction@infraction/_scheduler.py`, we plan on extracting the 3 different try/exepct blocks into separate methods (pardon_infraction, user_is_watched, update_db). - For `humanize_delta@utils/time.py`, we plan on extracting methods, as the function is composed of two main parts, parsing of overload arguments into time delta, and stringification of the delta. Arguably, the former can be delegated to a separate helper function, which should greatly reduce the cyclomatic complexity. - For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, we plan on extracting methods, as the function is composed of many steps that can be isolated into separate functions. One could extract functionalities like redefining invites, sorting invites, finding blocked invites and cleaning up invites into separate functions. - For `infraction_edit@infraction/management.py`, despite all code being relevant, we can still make changes to the complexity by refactoring and dividing the function into less complex functions which can be used by other functions in the future. More specifically, we can separate the rescheduling functionality from infraction_edit making a helper function which reschedules an infraction when necessary. Estimated impact of refactoring (lower CC, but other drawbacks?): -- For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. +- For `deactivate_infraction@infraction/_scheduler.py`, some drawbacks are decreased readablility of the code and increased function calls. +- For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. - For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, no drawbacks are anticipated. - For `infraction_edit@infraction/management.py`, it should decrease the CC from 18 to 11, which is a decrease of about 39%. No drawbacks are anticipated. From 948e3ddd571f9d63e161573e2c1b04f5c30c2645 Mon Sep 17 00:00:00 2001 From: Celleforst <77944684+Celleforst@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:59:42 +0100 Subject: [PATCH 16/24] docs(#35): update reportmd with marcellos coverage improvements (#52) * docs(#35): add invite.py coverage to report Add coverage invite.py info to report * docs(#35): add missing lines to report Adds missing lines for coverage improvement for invite.py to report * docs(#35): add reamining info need for report Adds test description and link to PR to report for coverage improvement of `apply_for` --- report.md | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/report.md b/report.md index f41c1ea6a8..055bb8a7c7 100644 --- a/report.md +++ b/report.md @@ -77,7 +77,7 @@ Plan for refactoring complex code: - For `infraction_edit@infraction/management.py`, despite all code being relevant, we can still make changes to the complexity by refactoring and dividing the function into less complex functions which can be used by other functions in the future. More specifically, we can separate the rescheduling functionality from infraction_edit making a helper function which reschedules an infraction when necessary. Estimated impact of refactoring (lower CC, but other drawbacks?): -- For `deactivate_infraction@infraction/_scheduler.py`, some drawbacks are decreased readablility of the code and increased function calls. +- For `deactivate_infraction@infraction/_scheduler.py`, some drawbacks are decreased readablility of the code and increased function calls. - For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. - For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, no drawbacks are anticipated. - For `infraction_edit@infraction/management.py`, it should decrease the CC from 18 to 11, which is a decrease of about 39%. No drawbacks are anticipated. @@ -104,7 +104,7 @@ There is only one small caveat here: coverage by function or class is not native ### Your own coverage tool Show a patch (or link to a branch) that shows the instrumented code to -gather coverage measurements: +gather coverage measurements: - For `humanize_delta@utils/time.py`, we have [PR #10](https://github.com/dd2480-spring-2025-group-1/bot/pull/10) - For `infraction_edit@infraction/management.py`: we have [PR #28](https://github.com/dd2480-spring-2025-group-1/bot/pull/28) @@ -146,26 +146,36 @@ Show the comments that describe the requirements for the coverage: - The function should not allow editing the duration of a warning or note infraction. - The function should not allow editing the duration of an expired infraction. - The function should call the `api_client.patch` method to update an infraction when a new reason is provided. - +- For `apply_fpr@./bot/exts/filtering/_filter_lists/invite.py` there were no tests before. The function documentation is very scarce, thus the test cases had to be derived from the code itself. Some parts of the code are inaccessible since they require certain filters to trigger, which is not realizable with the MockBot used which generates random ids and data. +Nonetheless the following tests could be implemented: + - The function should return success for a valid invite url, i.e. empty action, a message containing the invite code and the list filter that allowed the invite (since no filter triggered it, it should be ListType.ALLOW:[]). + - The function should return failure when there is no invite url in the ctx content, i.e. it should return None as action, an empty message and an empty dictionary for the list type. + - The function should return failure when the invite url is invalid. + - The function should return success for a different but valid url. Report of old coverage: ``` -Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------------------------ -bot/utils/helpers.py 23 8 4 1 67% 19, 25-28, 38-43 -infraction_edit@management.py 51 51 26 0 0% 192-281 +Name Stmts Miss Branch BrPart Cover Missing +------------------------------------------------------------------------------------------ +bot/utils/helpers.py 23 8 4 1 67% 19, 25-28, 38-43 +infraction_edit@management.py 51 51 26 0 0% 192-281 +bot/exts/filtering/_filter_lists/invite.py 93 69 32 1 20% 19, 47-48, 52, 57, 63-152, 157-172 ``` Report of new coverage: ``` -Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------------------------ -bot/utils/helpers.py 23 0 4 0 100% -infraction_edit@management.py 51 22 26 7 52% 188, 194-195, 197-202, 214, 229-242, 254-255, 261 +Name Stmts Miss Branch BrPart Cover Missing +------------------------------------------------------------------------------------------ +bot/utils/helpers.py 23 0 4 0 100% +infraction_edit@management.py 51 22 26 7 52% 188, 194-195, 197-202, 214, 229-242, 254-255, 261 +bot/exts/filtering/_filter_lists/invite.py 93 13 32 10 77% 19, 57, 76->78, 90-92, 96-97, 117->128, 129, 132->135, 136-140, 145->148, 161->166, 164 + + ``` Test cases added: - For `utils/helpers.py`, [PR #3260](https://github.com/python-discord/bot/pull/3260) had been created by @strengthless, approved and merged into the upstream, which included 7 new test cases. - For `infraction_edit@infraction/management.py`, [PR #38](https://github.com/dd2480-spring-2025-group-1/bot/pull/38) has been drafted. +- For `apply_fpr@./bot/exts/filtering/_filter_lists/invite.py`, [PR #44](https://github.com/dd2480-spring-2025-group-1/bot/pull/44) has been drafted. ## Self-assessment: Way of working From 2341b8dd196691b673dd108465d90249ed52759c Mon Sep 17 00:00:00 2001 From: OllanBollan <84289304+OllanBollan@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:12:44 +0100 Subject: [PATCH 17/24] docs(#51) Added self-assessment WoW (#55) --- report.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/report.md b/report.md index 055bb8a7c7..14c82a1966 100644 --- a/report.md +++ b/report.md @@ -179,13 +179,9 @@ Test cases added: ## Self-assessment: Way of working -Current state according to the Essence standard: ... +We would now argue that we have reached the “In Place” state. We previously estimated that we were close to reaching “In Place” but had to work more as a group on modifying and improving the practices in the group. For this project all group members have participated in trying to modify and improve the Ways of Working. -Was the self-assessment unanimous? Any doubts about certain items? - -How have you improved so far? - -Where is potential for improvement? +To reach the next state “Working Well” we would need to work on becoming more comfortable with the practices, in order to apply them naturally without thinking about them. Additionally we would need to improve the “continually tunes their use of the practices and tools." ## Overall experience From 570106868d034302e3c034bcf66bff583e14a3ba Mon Sep 17 00:00:00 2001 From: Johan <104634655+joel90688@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:14:17 +0100 Subject: [PATCH 18/24] docs(#50): wrote overall experience (#53) Co-authored-by: Johan Nilsson --- report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/report.md b/report.md index 14c82a1966..f74fdef8af 100644 --- a/report.md +++ b/report.md @@ -186,5 +186,6 @@ To reach the next state “Working Well” we would need to work on becoming mor ## Overall experience What are your main take-aways from this project? What did you learn? +One takeaway from this project is that GitHub automatically wants to make pull requests towards the main repository and not the fork. This caused us to bloat the main repository with faulty PRs. More importantly, we learned about function coverage and how to calculate it, helping us write better test cases. The hardest thing to learn was how to implement our own tests in a project built by someone else, many struggled with seemingly random errors that required them to thoroughly read the code and learn how it works. This lesson will be useful for all of us when we contribute to other projects in the future. As an additional note for P+, we have a working patch ([PR #3260](https://github.com/python-discord/bot/pull/3260)) accepted and merged into the upstream, which included a small fix along with the addition of 7 new test cases. From 18cf30d5312842fb5b856599a0dd2e0a2f09525a Mon Sep 17 00:00:00 2001 From: Strengthless Date: Fri, 21 Feb 2025 13:50:55 +0100 Subject: [PATCH 19/24] docs(#56): update `report.md` with persons attempting P+ --- report.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/report.md b/report.md index f74fdef8af..20445a4922 100644 --- a/report.md +++ b/report.md @@ -152,6 +152,7 @@ Nonetheless the following tests could be implemented: - The function should return failure when there is no invite url in the ctx content, i.e. it should return None as action, an empty message and an empty dictionary for the list type. - The function should return failure when the invite url is invalid. - The function should return success for a different but valid url. + Report of old coverage: ``` Name Stmts Miss Branch BrPart Cover Missing @@ -179,7 +180,7 @@ Test cases added: ## Self-assessment: Way of working -We would now argue that we have reached the “In Place” state. We previously estimated that we were close to reaching “In Place” but had to work more as a group on modifying and improving the practices in the group. For this project all group members have participated in trying to modify and improve the Ways of Working. +We would now argue that we have reached the “In Place” state. We previously estimated that we were close to reaching “In Place” but had to work more as a group on modifying and improving the practices in the group. For this project all group members have participated in trying to modify and improve the Ways of Working. To reach the next state “Working Well” we would need to work on becoming more comfortable with the practices, in order to apply them naturally without thinking about them. Additionally we would need to improve the “continually tunes their use of the practices and tools." @@ -188,4 +189,4 @@ To reach the next state “Working Well” we would need to work on becoming mor What are your main take-aways from this project? What did you learn? One takeaway from this project is that GitHub automatically wants to make pull requests towards the main repository and not the fork. This caused us to bloat the main repository with faulty PRs. More importantly, we learned about function coverage and how to calculate it, helping us write better test cases. The hardest thing to learn was how to implement our own tests in a project built by someone else, many struggled with seemingly random errors that required them to thoroughly read the code and learn how it works. This lesson will be useful for all of us when we contribute to other projects in the future. -As an additional note for P+, we have a working patch ([PR #3260](https://github.com/python-discord/bot/pull/3260)) accepted and merged into the upstream, which included a small fix along with the addition of 7 new test cases. +As an additional note for P+, we have a working patch ([PR #3260](https://github.com/python-discord/bot/pull/3260)) accepted and merged into the upstream, which included a small fix along with the addition of 7 new test cases. The persons who are aiming for P+ are Johan (@joel90688), Kim (@strengthless) and Marcello (@Celleforst) From af601fb160eeb5b780c1fe01a1108817aa74562e Mon Sep 17 00:00:00 2001 From: Johan <104634655+joel90688@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:15:58 +0100 Subject: [PATCH 20/24] docs(#60): reworded link to coverage tool for infraction_edit (#61) Co-authored-by: Johan Nilsson --- report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/report.md b/report.md index 20445a4922..f36603e53b 100644 --- a/report.md +++ b/report.md @@ -106,7 +106,7 @@ There is only one small caveat here: coverage by function or class is not native Show a patch (or link to a branch) that shows the instrumented code to gather coverage measurements: - For `humanize_delta@utils/time.py`, we have [PR #10](https://github.com/dd2480-spring-2025-group-1/bot/pull/10) -- For `infraction_edit@infraction/management.py`: we have [PR #28](https://github.com/dd2480-spring-2025-group-1/bot/pull/28) +- For `infraction_edit@infraction/management.py`: we have [PR #28](https://github.com/dd2480-spring-2025-group-1/bot/pull/28). This PR includes created tests from later in the assignment since the coverage tool will otherwise not run if there are no tests as described in the limitations of the tool. What kinds of constructs does your tool support, and how accurate is its output? From 4e6fab8c9b2ea62cdba46fc4a593e18565160bca Mon Sep 17 00:00:00 2001 From: Arvid Hjort <73788234+HerodrawzZ@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:21:44 +0100 Subject: [PATCH 21/24] doc(#57): Added refactoring plan for Arvid (#62) --- report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/report.md b/report.md index f36603e53b..4038ae09a6 100644 --- a/report.md +++ b/report.md @@ -71,12 +71,14 @@ With everything combined, we deemed the project suitable for this assignment. ## Refactoring Plan for refactoring complex code: +- For `apply_infraction@infraction/_scheduler.py`, we can extract most of the code related to logging of the results. This code be handled in a seperate function. This would reduce the amount of CCN significantly. - For `deactivate_infraction@infraction/_scheduler.py`, we plan on extracting the 3 different try/exepct blocks into separate methods (pardon_infraction, user_is_watched, update_db). - For `humanize_delta@utils/time.py`, we plan on extracting methods, as the function is composed of two main parts, parsing of overload arguments into time delta, and stringification of the delta. Arguably, the former can be delegated to a separate helper function, which should greatly reduce the cyclomatic complexity. - For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, we plan on extracting methods, as the function is composed of many steps that can be isolated into separate functions. One could extract functionalities like redefining invites, sorting invites, finding blocked invites and cleaning up invites into separate functions. - For `infraction_edit@infraction/management.py`, despite all code being relevant, we can still make changes to the complexity by refactoring and dividing the function into less complex functions which can be used by other functions in the future. More specifically, we can separate the rescheduling functionality from infraction_edit making a helper function which reschedules an infraction when necessary. Estimated impact of refactoring (lower CC, but other drawbacks?): +- For `apply_infraction@infraction/_scheduler.py`, no particular drawbacks should arise. It should decrease the CC with about 40%. - For `deactivate_infraction@infraction/_scheduler.py`, some drawbacks are decreased readablility of the code and increased function calls. - For `humanize_delta@utils/time.py`, no drawbacks are anticipated, except for the use of `typing.Any` in the type signature for the new helper function. However, since type hints are not strongly enforced in Python (they're just **hints** for humans), this should not be a huge deal. - For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, no drawbacks are anticipated. From 4db164a6f4733b3f2b9e33ee785728f65b99ce39 Mon Sep 17 00:00:00 2001 From: Celleforst <77944684+Celleforst@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:37:26 +0100 Subject: [PATCH 22/24] docs(#59): Fix small typos and update coverage report (#63) Closes issue #59 by fixing several typos. --- report.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/report.md b/report.md index 4038ae09a6..7fade7d920 100644 --- a/report.md +++ b/report.md @@ -109,6 +109,7 @@ Show a patch (or link to a branch) that shows the instrumented code to gather coverage measurements: - For `humanize_delta@utils/time.py`, we have [PR #10](https://github.com/dd2480-spring-2025-group-1/bot/pull/10) - For `infraction_edit@infraction/management.py`: we have [PR #28](https://github.com/dd2480-spring-2025-group-1/bot/pull/28). This PR includes created tests from later in the assignment since the coverage tool will otherwise not run if there are no tests as described in the limitations of the tool. +- For `actions_for@invite.py`: we have [PR #46](https://github.com/dd2480-spring-2025-group-1/bot/pull/46). This PR includes created tests from later in the assignment since the coverage tool will otherwise not run if there are no tests as described in the limitations of the tool. What kinds of constructs does your tool support, and how accurate is its output? @@ -148,7 +149,7 @@ Show the comments that describe the requirements for the coverage: - The function should not allow editing the duration of a warning or note infraction. - The function should not allow editing the duration of an expired infraction. - The function should call the `api_client.patch` method to update an infraction when a new reason is provided. -- For `apply_fpr@./bot/exts/filtering/_filter_lists/invite.py` there were no tests before. The function documentation is very scarce, thus the test cases had to be derived from the code itself. Some parts of the code are inaccessible since they require certain filters to trigger, which is not realizable with the MockBot used which generates random ids and data. +- For `actions_for@_filter_lists/invite.py` there were no tests before. The function documentation is very scarce, thus the test cases had to be derived from the code itself. Some parts of the code are inaccessible since they require certain filters to trigger, which is not realizable with the MockBot used which generates random ids and data. Nonetheless the following tests could be implemented: - The function should return success for a valid invite url, i.e. empty action, a message containing the invite code and the list filter that allowed the invite (since no filter triggered it, it should be ListType.ALLOW:[]). - The function should return failure when there is no invite url in the ctx content, i.e. it should return None as action, an empty message and an empty dictionary for the list type. @@ -161,7 +162,7 @@ Name Stmts Miss Branch BrPart Cover ------------------------------------------------------------------------------------------ bot/utils/helpers.py 23 8 4 1 67% 19, 25-28, 38-43 infraction_edit@management.py 51 51 26 0 0% 192-281 -bot/exts/filtering/_filter_lists/invite.py 93 69 32 1 20% 19, 47-48, 52, 57, 63-152, 157-172 +actions_for@invite.py 55 55 26 0 0% 63-152 ``` Report of new coverage: @@ -170,15 +171,14 @@ Name Stmts Miss Branch BrPart Cover ------------------------------------------------------------------------------------------ bot/utils/helpers.py 23 0 4 0 100% infraction_edit@management.py 51 22 26 7 52% 188, 194-195, 197-202, 214, 229-242, 254-255, 261 -bot/exts/filtering/_filter_lists/invite.py 93 13 32 10 77% 19, 57, 76->78, 90-92, 96-97, 117->128, 129, 132->135, 136-140, 145->148, 161->166, 164 - +actions_for@invite.py 55 10 26 7 72% 76->78, 90-92, 96-97, 117->128, 129, 132->135, 136-140, 145->148 ``` Test cases added: - For `utils/helpers.py`, [PR #3260](https://github.com/python-discord/bot/pull/3260) had been created by @strengthless, approved and merged into the upstream, which included 7 new test cases. - For `infraction_edit@infraction/management.py`, [PR #38](https://github.com/dd2480-spring-2025-group-1/bot/pull/38) has been drafted. -- For `apply_fpr@./bot/exts/filtering/_filter_lists/invite.py`, [PR #44](https://github.com/dd2480-spring-2025-group-1/bot/pull/44) has been drafted. +- For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, [PR #44](https://github.com/dd2480-spring-2025-group-1/bot/pull/44) has been drafted. ## Self-assessment: Way of working From 49f2cfdd909d416b9198ab8b00629be0371961e7 Mon Sep 17 00:00:00 2001 From: Strengthless Date: Fri, 21 Feb 2025 14:49:46 +0100 Subject: [PATCH 23/24] docs(#66): update `report.md` with findings from #64 --- report.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/report.md b/report.md index 7fade7d920..e9cd6a7403 100644 --- a/report.md +++ b/report.md @@ -89,7 +89,7 @@ Carried out refactoring (optional, P+): - For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, we have [PR #22](https://github.com/dd2480-spring-2025-group-1/bot/pull/22) which reduces CCN by 35.1%. - For `infraction_edit@infraction/management.py`, we have [PR #21](https://github.com/dd2480-spring-2025-group-1/bot/pull/21) which reduces CCN by about 39%. -Note: since `on_command_error@error_handler.py` already has 100% test coverage as reported by `coverage.py`, we decided to do part 2 of the assignment with `actions_for@invite.py` instead, which is the function with the highest CCN as reported by lizard (CCN 37) and it has 20% test coverage. +Note: since `on_command_error@error_handler.py` already has 100% test coverage as reported by `coverage.py`, we decided to do part 2 of the assignment with `actions_for@invite.py` instead, which is the function with the highest CCN as reported by lizard (CCN 37) and it has 0% test coverage. ## Coverage @@ -133,7 +133,8 @@ its output? - Another limitation is that there will be no coverage output if no tests exist for the function since the coverage tool will never be run. 3. Are the results of your tool consistent with existing coverage tools? -- The reported number of total branches are consistent. +- The reported number of total branches are mostly consistent. + - Except for `deactivate_infraction@_scheduler.py`, because `coverage.py` is not taking one-liners (e.g., `x if y else z`, `for x in y`) into account, while our tool does. - However, the numbers of missing branches (which you can obtain using `poetry run coverage json ./bot/utils/time.py`) are different. - Upon further investigation, we realized that it's because our tool was only checking against `test_time.py` for branch coverage on `time.py`. On the contrary, `coverage.py` records all LoC transitions when running **all** test files, then compares them against a list of possible branch transitions (statically analysed), which yields the final branch coverage report ([ref](https://coverage.readthedocs.io/en/7.6.12/branch.html#how-it-works)). - For example, let's assume `humanize_delta@time.py` is used in the function `get_slowmode@slowmode.py`, and `get_slowmode` is tested in `test_slowmode.py`. When the test suite for `test_slowmode.py` runs, some branches within `humanize_delta@time.py` will also be executed, and thereby increasing the branch coverage on `time.py`, despite it not being tested directly. @@ -189,6 +190,6 @@ To reach the next state “Working Well” we would need to work on becoming mor ## Overall experience What are your main take-aways from this project? What did you learn? -One takeaway from this project is that GitHub automatically wants to make pull requests towards the main repository and not the fork. This caused us to bloat the main repository with faulty PRs. More importantly, we learned about function coverage and how to calculate it, helping us write better test cases. The hardest thing to learn was how to implement our own tests in a project built by someone else, many struggled with seemingly random errors that required them to thoroughly read the code and learn how it works. This lesson will be useful for all of us when we contribute to other projects in the future. +- One takeaway from this project is that GitHub automatically wants to make pull requests towards the main repository and not the fork. This caused us to bloat the main repository with faulty PRs. More importantly, we learned about function coverage and how to calculate it, helping us write better test cases. The hardest thing to learn was how to implement our own tests in a project built by someone else, many struggled with seemingly random errors that required them to thoroughly read the code and learn how it works. This lesson will be useful for all of us when we contribute to other projects in the future. As an additional note for P+, we have a working patch ([PR #3260](https://github.com/python-discord/bot/pull/3260)) accepted and merged into the upstream, which included a small fix along with the addition of 7 new test cases. The persons who are aiming for P+ are Johan (@joel90688), Kim (@strengthless) and Marcello (@Celleforst) From 1543c0d5dcc70a6ca5f6c83f1e5a7b22e0af1a9a Mon Sep 17 00:00:00 2001 From: Arvid Hjort Date: Fri, 21 Feb 2025 15:45:38 +0100 Subject: [PATCH 24/24] docs(#67): Added coverage improvment for apply_infraction --- report.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/report.md b/report.md index e9cd6a7403..b69b31fd31 100644 --- a/report.md +++ b/report.md @@ -144,6 +144,7 @@ its output? ## Coverage improvement Show the comments that describe the requirements for the coverage: +- For `apply_infraction@infraction/_scheduler.py`, the code had zero coverage since before the implementation of the two new unittest. One unittest simply tries to run through the function successfully and the other fails by raising a keyerror. - For `utils/helpers.py`, the functions are fairly straightforward. The requirements were already well documented in the one-line docstrings. The only caveat here is the `has_lines` function, which ignores one `\n` character from the end of the string when counting the number of lines. - For `infraction_edit@infraction/management.py`, there were no tests before but the documentation of the function was clear and helped in creating the requirements, e.g.: - The function should raise a BadArgument when a duration and a reason is not provided. @@ -161,6 +162,7 @@ Report of old coverage: ``` Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------------------------------ +apply_infraction@_scheduler.py 86 86 34 0 0% 147-300 bot/utils/helpers.py 23 8 4 1 67% 19, 25-28, 38-43 infraction_edit@management.py 51 51 26 0 0% 192-281 actions_for@invite.py 55 55 26 0 0% 63-152 @@ -170,6 +172,7 @@ Report of new coverage: ``` Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------------------------------ +apply_infraction@_scheduler.py 86 41 34 12 48% 157->160, 167, 183->191 ....221-238, 241->259, 246-256, 260-267 bot/utils/helpers.py 23 0 4 0 100% infraction_edit@management.py 51 22 26 7 52% 188, 194-195, 197-202, 214, 229-242, 254-255, 261 actions_for@invite.py 55 10 26 7 72% 76->78, 90-92, 96-97, 117->128, 129, 132->135, 136-140, 145->148 @@ -177,9 +180,10 @@ actions_for@invite.py 55 10 26 7 72% ``` Test cases added: +- For `apply_infraction@infraction/_scheduler.py`, [PR #54](https://github.com/dd2480-spring-2025-group-1/bot/pull/54) has been drafted, 2 test cases added. - For `utils/helpers.py`, [PR #3260](https://github.com/python-discord/bot/pull/3260) had been created by @strengthless, approved and merged into the upstream, which included 7 new test cases. -- For `infraction_edit@infraction/management.py`, [PR #38](https://github.com/dd2480-spring-2025-group-1/bot/pull/38) has been drafted. -- For `actions_for@./bot/exts/filtering/_filter_lists/invite.py`, [PR #44](https://github.com/dd2480-spring-2025-group-1/bot/pull/44) has been drafted. +- For `infraction_edit@infraction/management.py`, [PR #38](https://github.com/dd2480-spring-2025-group-1/bot/pull/38) has been drafted, 4 test cases added. +- For `actions_for@_filter_lists/invite.py`, [PR #44](https://github.com/dd2480-spring-2025-group-1/bot/pull/44) has been drafted, 4 test cases added. ## Self-assessment: Way of working