├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ └── FEATURE_REQUEST.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── assets ├── benchmark.html ├── benchmark.png ├── correct.png └── tomd_cant_handle.png ├── poetry.lock ├── pyproject.toml ├── tests ├── __init__.py ├── test_edgecases.py ├── test_roundtrip.py └── test_version.py └── unmarkd ├── __init__.py └── unmarkers.py /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 🤗 10 | - type: checkboxes 11 | id: generic-bug-report-checklist 12 | attributes: 13 | label: Prerequisites 14 | description: "Before you continue make sure you have done all of the following:" 15 | options: 16 | - label: I have searched the [issues](https://github.com/ThatXliner/unmarkd/issues) and believe that it has not already been reported 17 | required: true 18 | - label: I have made sure this bug reproduces on the latest version 19 | required: true 20 | - label: "I agree to follow the [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)" 21 | required: true 22 | - type: textarea 23 | id: bug-info 24 | attributes: 25 | label: Bug description 26 | description: Also tell us, what did you expect to happen? 27 | placeholder: Tell us what you see! What did you think should've happened? 28 | value: "A bug happened! There shouldn't be!" 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: repro 33 | attributes: 34 | label: Reproduction steps 35 | description: What are the steps that you took to create this bug? 36 | placeholder: | 37 | 1. In this environment... 38 | 2. With this config... 39 | 3. Run '...' 40 | 4. See error... 41 | value: "1. " 42 | validations: 43 | required: true 44 | - type: textarea 45 | id: other-info 46 | attributes: 47 | label: Other information 48 | description: Other additional information that may deem useful. If you're sending code, please make an [MCVE](https://stackoverflow.com/help/minimal-reproducible-example) 49 | placeholder: "Environment, OS, debug logs, etc." 50 | validations: 51 | required: false 52 | - type: input 53 | id: repro-how-often 54 | attributes: 55 | label: Reproduces how often 56 | description: What percentage of the time does it reproduce? 57 | placeholder: "100% of the time? 75% of the times? Only when I do X?" 58 | value: "I can reproduce this bug 100% of the time" 59 | validations: 60 | required: true 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature request 2 | description: Request a new feature, or a change in an existing one. 3 | title: "[REQUEST]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: checkboxes 7 | id: generic-feature-request-checklist 8 | attributes: 9 | label: Prerequisites 10 | description: "Before you start, make sure you have done all of the following:" 11 | options: 12 | - label: I have searched the [issues](https://github.com/ThatXliner/unmarkd/issues) and believe that it has not already been requested (and possibly rejected) 13 | required: true 14 | - label: I have made sure that this feature isn't possible with this project's plugin system and/or configuration; core changes must be made to make this happen. 15 | required: true 16 | - label: "I agree to follow the [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)" 17 | required: true 18 | - type: textarea 19 | id: feature-info 20 | attributes: 21 | label: Feature description 22 | description: Describe the feature 23 | placeholder: Tell us what you want to happen, and why can't it happen with our project's current customization (if you want to request enhancements to the plugin system/configuration/customization system, please file a seperate feature request for that) 24 | value: "Example: I want X which does Y, and this is a core change/I can't do right now because of Z" 25 | validations: 26 | required: true 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | schedule: 4 | - cron: "0 0 1 * *" # Run every month 5 | # This way, we can make sure code doesn't break via external dependencies 6 | push: 7 | pull_request: 8 | env: 9 | POETRY_VERSION: 1.5.1 10 | jobs: 11 | test: 12 | strategy: 13 | matrix: 14 | python-version: 15 | - 3.8 16 | - 3.9 17 | - "3.10" 18 | - "3.11" 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - name: Check out repository code 23 | uses: actions/checkout@v2 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - uses: actions/cache@v3 29 | with: 30 | path: | 31 | .venv 32 | .pytest_cache 33 | key: ${{ runner.os }}-pip-${{ hashFiles('**/poetry.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-pip- 36 | 37 | - name: Install Poetry and Poe (UNIX) 38 | if: runner.os != 'Windows' 39 | run: python3 -m pip install poetry==$POETRY_VERSION poethepoet 40 | - name: Install Poetry and Poe (Windows) 41 | if: runner.os == 'Windows' 42 | run: python -m pip install poetry==$env:POETRY_VERSION poethepoet 43 | 44 | - name: Install dependencies 45 | run: | 46 | poetry config virtualenvs.in-project true 47 | poetry install 48 | poetry install # Second time to install the project 49 | - name: Run test suite 50 | run: poe ci 51 | - name: Upload to CodeCov 52 | uses: codecov/codecov-action@v2 53 | build: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Check out repository code 57 | uses: actions/checkout@v2 58 | - uses: actions/setup-python@v4 59 | with: 60 | python-version: 3 61 | - name: Install Poetry $POETRY_VERSION 62 | run: python3 -m pip install --upgrade pip poetry==$POETRY_VERSION 63 | - name: Build project 64 | run: poetry build 65 | lints: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Check out repository code 69 | uses: actions/checkout@v2 70 | - name: Set up Python 71 | uses: actions/setup-python@v4 72 | with: 73 | python-version: 3 74 | - uses: actions/cache@v3 75 | with: 76 | path: .venv 77 | key: ${{ runner.os }}-pip-${{ hashFiles('**/poetry.lock') }} 78 | restore-keys: | 79 | ${{ runner.os }}-pip- 80 | 81 | - name: Install dependencies 82 | run: | 83 | python3 -m pip install --upgrade pip poetry==$POETRY_VERSION poethepoet 84 | poetry config virtualenvs.in-project true 85 | poetry install 86 | poetry install # Second time to install the project 87 | 88 | - name: Run style lints 89 | run: poe style 90 | - name: Run correctness lints 91 | run: poe codebase 92 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Asset Uploader 2 | on: 3 | release: 4 | types: 5 | - created 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-python@v4 12 | with: 13 | python-version: 3 14 | - name: Install Poetry 15 | uses: snok/install-poetry@v1 16 | - name: Build artifacts 17 | run: poetry build 18 | - name: Upload artifacts 19 | uses: shogo82148/actions-upload-release-asset@v1 20 | with: 21 | upload_url: ${{ github.event.release.upload_url }} 22 | asset_path: dist/* 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | __pycache__/ 3 | .hypothesis/ 4 | .pytest_cache/ 5 | .DS_Store 6 | .mypy_cache/ 7 | .env 8 | htmlcov/ 9 | dist/ 10 | build/ 11 | .coverage* 12 | coverage.xml 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests", "-vvv" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.nosetestsEnabled": false, 7 | "python.testing.pytestEnabled": true, 8 | "python.linting.mypyEnabled": true, 9 | "python.linting.pylintEnabled": false, 10 | "python.linting.enabled": true, 11 | "python.formatting.provider": "black" 12 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.1 4 | - Fixed bugs relating nested structures ([#3](https://github.com/ThatXliner/unmarkd/pull/3)) 5 | - Updated parameter types for `unmarkd.unmark` ([94fe973](https://github.com/ThatXliner/unmarkd/commit/94fe9733677a8ad262f5a48affbc29a8616a2baf)) 6 | - Added more metadata ([8f80ec8](https://github.com/ThatXliner/unmarkd/commit/8f80ec86ad30fe77218b1fcd47869aed08e138d8)) 7 | - Added license ([4a47dca](https://github.com/ThatXliner/unmarkd/commit/4a47dca88ffa23b9a7daa0fa19fbcf658658b2c9)) 8 | ## 0.1.0 9 | - Initial release ([ee262b9](https://github.com/ThatXliner/unmarkd/commit/ee262b9ff3c844f2b89a57eb77b0836be1a9b3aa)) 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request Check List 2 | 3 | 4 | **Applicable issues** 5 | 6 | 13 | 14 | **Description of PR** 15 | 16 | 17 | 18 | **Alternative designs considered** 19 | 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE: This project is _maintained._** While it may seem inactive, it is because there is nothing to add. If you want an enhancement or want to file a bug report, please go to the [issues](https://github.com/ThatXliner/unmarkd/issues). 2 | 3 | # 🔄 Unmarkd 4 | 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json)](https://github.com/charliermarsh/ruff) 7 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) 8 | [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)[![codecov](https://codecov.io/gh/ThatXliner/unmarkd/branch/master/graph/badge.svg?token=PWVIERHTG3)](https://codecov.io/gh/ThatXliner/unmarkd) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![CI](https://github.com/ThatXliner/unmarkd/actions/workflows/ci.yml/badge.svg)](https://github.com/ThatXliner/unmarkd/actions/workflows/ci.yml) [![PyPI - Downloads](https://img.shields.io/pypi/dm/unmarkd)](https://pypi.org/project/unmarkd/) 9 | 10 | > A markdown reverser. 11 | 12 | --- 13 | 14 | Unmarkd is a [BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)-powered [Markdown](https://en.wikipedia.org/wiki/Markdown) reverser written in Python and for Python. 15 | 16 | ## Why 17 | 18 | This is created as a [StackSearch](http://github.com/ThatXliner/stacksearch) (one of my other projects) dependency. In order to create a better API, I needed a way to reverse HTML. So I created this. 19 | 20 | There are [similar projects](https://github.com/xijo/reverse_markdown) (written in Ruby) ~~but I have not found any written in Python (or for Python)~~ later I found a popular library, [html2text](https://github.com/Alir3z4/html2text). 21 | 22 | ## Installation 23 | 24 | You know the drill 25 | 26 | ```bash 27 | pip install unmarkd 28 | ``` 29 | 30 | ## Comparison 31 | 32 | **TL;DR: Html2Text is fast. If you don't need much configuration, you could use Html2Text for the little speed increase.** 33 | 34 |
35 | 36 | Click to expand 37 | 38 | ### Speed 39 | 40 | **TL;DR: Unmarkd < Html2Text** 41 | 42 | Html2Text is basically faster: 43 | 44 | ![Benchmark](./assets/benchmark.png) 45 | 46 | (The `DOC` variable used can be found [here](./assets/benchmark.html)) 47 | 48 | Unmarkd sacrifices speed for [power](#configurability). 49 | 50 | Html2Text directly uses Python's [`html.parser`](https://docs.python.org/3/library/html.parser.html) module (in the standard library). On the other hand, Unmarkd uses the powerful HTML parsing library, `beautifulsoup4`. BeautifulSoup can be configured to use different HTML parsers. In Unmarkd, we configure it to use Python's `html.parser`, too. 51 | 52 | But another layer of code means more code is ran. 53 | 54 | I hope that's a good explanation of the speed difference. 55 | 56 | ### Correctness 57 | 58 | **TL;DR: Unmarkd == Html2Text** 59 | 60 | I actually found _two_ html-to-markdown libraries. One of them was [Tomd](https://github.com/gaojiuli/tomd) which had an _incorrect implementation_: 61 | 62 | ![Actual results](./assets/tomd_cant_handle.png) 63 | 64 | It seems to be abandoned, anyway. 65 | 66 | Now with Html2Text and Unmarkd: 67 | 68 | ![Epic showdown](./assets/correct.png) 69 | 70 | In other words, they _work_ 71 | 72 | ### Configurability 73 | 74 | **TL;DR: Unmarkd > Html2Text** 75 | 76 | This is Unmarkd's strong point. 77 | 78 | In Html2Text, you only have a limited [set of options](https://github.com/Alir3z4/html2text/blob/master/docs/usage.md#available-options). 79 | 80 | In Unmarkd, you can subclass the `BaseUnmarker` and implement conversions for new tags (e.g. ``), etc. In my opinion, it's much easier to extend and configure Unmarkd. 81 | 82 | Unmarkd was originally written as a StackSearch dependancy. 83 | 84 | Html2Text has no options for configuring parsing of code blocks. Unmarkd does 85 | 86 |
87 | 88 | ## Documentation 89 | 90 | Here's an example of basic usage 91 | 92 | ```python 93 | import unmarkd 94 | print(unmarkd.unmark("I love markdown!")) 95 | # Output: **I *love* markdown!** 96 | ``` 97 | 98 | or something more complex (shamelessly taken from [here](https://markdowntohtml.com)): 99 | 100 | ```python 101 | import unmarkd 102 | html_doc = R"""

Sample Markdown

103 |

This is some basic, sample markdown.

104 |

Second Heading

105 |
    106 |
  • Unordered lists, and:
      107 |
    1. One
    2. 108 |
    3. Two
    4. 109 |
    5. Three
    6. 110 |
    111 |
  • 112 |
  • More
  • 113 |
114 |
115 |

Blockquote

116 |
117 |

And bold, italics, and even italics and later bold. Even strikethrough. A link to somewhere.

118 |

And code highlighting:

119 |
var foo = 'bar';
120 | 
121 | function baz(s) {
122 |    return foo + ':' + s;
123 | }
124 | 
125 |

Or inline code like var foo = 'bar';.

126 |

Or an image of bears

127 |

bears

128 |

The end ...

129 | """ 130 | print(unmarkd.unmark(html_doc)) 131 | ``` 132 | 133 | and the output: 134 | 135 | ````markdown 136 | # Sample Markdown 137 | 138 | 139 | This is some basic, sample markdown. 140 | 141 | ## Second Heading 142 | 143 | 144 | 145 | - Unordered lists, and: 146 | 1. One 147 | 2. Two 148 | 3. Three 149 | - More 150 | 151 | >Blockquote 152 | 153 | 154 | And **bold**, *italics*, and even *italics and later **bold***. Even ~~strikethrough~~. [A link](https://markdowntohtml.com) to somewhere. 155 | 156 | And code highlighting: 157 | 158 | 159 | ```js 160 | var foo = 'bar'; 161 | 162 | function baz(s) { 163 | return foo + ':' + s; 164 | } 165 | ``` 166 | 167 | 168 | Or inline code like `var foo = 'bar';`. 169 | 170 | Or an image of bears 171 | 172 | ![bears](http://placebear.com/200/200) 173 | 174 | The end ... 175 | ```` 176 | 177 | ### Extending 178 | 179 | #### Brief Overview 180 | 181 | Most functionality should be covered by the `BasicUnmarker` class defined in `unmarkd.unmarkers`. 182 | 183 | If you need to reverse markdown from StackExchange (as in the case for my other project), you may use the `StackOverflowUnmarker` (or it's alias, `StackExchangeUnmarker`), which is also defined in `unmarkd.unmarkers`. 184 | 185 | #### Customizing 186 | 187 | If the above two classes do not suit your needs, you can subclass the `unmarkd.unmarkers.BaseUnmarker` abstract class. 188 | 189 | Currently, you can _optionally_ override the following methods: 190 | 191 | - `detect_language` (parameters: **1**) 192 | - **Parameters**: 193 | - html: `bs4.BeautifulSoup` 194 | - When a fenced code block is approached, this function is called with a parameter of type `bs4.BeautifulSoup` passed to it; this is the element the code block was detected from (i.e. `pre`). 195 | - This function is responsible for detecting the programming language (or returning `''` if none was detected) of the code block. 196 | - Note: This method is different from `unmarkd.unmarkers.BasicUnmarker`. It is simpler and does less checking/filtering 197 | 198 | But Unmarkd is more flexible than that. 199 | 200 | ##### Customizable constants 201 | 202 | There are currently 3 constants you may override: 203 | 204 | - Formats: 205 | NOTE: Use the [**Format String Syntax**](https://docs.python.org/3/library/string.html#formatstrings) 206 | - `UNORDERED_FORMAT` 207 | - The string format of unordered (bulleted) lists. 208 | - `ORDERED_FORMAT` 209 | - The string format of ordered (numbered) lists. 210 | - Miscellaneous: 211 | - `ESCAPABLES` 212 | - A container (preferably a `set`) of length-1 `str` that should be escaped 213 | 214 | ##### Customize converting HTML tags 215 | 216 | For an HTML tag `some_tag`, you can customize how it's converted to markdown by overriding a method like so: 217 | 218 | ```python 219 | from unmarkd.unmarkers import BaseUnmarker 220 | class MyCustomUnmarker(BaseUnmarker): 221 | def tag_some_tag(self, element) -> str: 222 | ... # parse code here 223 | ``` 224 | 225 | To reduce code duplication, if your tag also has aliases (e.g. `strong` is an alias for `b` in HTML) then you may modify the `TAG_ALIASES`. 226 | 227 | If you really need to, you may also modify `DEFAULT_TAG_ALIASES`. Be warned: if you do so, **you will also need to implement the aliases** (currently `em` and `strong`). 228 | 229 | ###### Common Patterns 230 | 231 | I find myself iterating through the children of the tag a lot. But that would lead to us needing to handle new tags, which could be anything. So here's the template/pattern I recommend: 232 | 233 | ```python 234 | from unmarkd.unmarkers import BaseUnmarker 235 | class MyCustomUnmarker(BaseUnmarker): 236 | def tag_some_tag(self, element) -> str: 237 | for child in element.children: 238 | if non_tag_output := self.parse_non_tags(child): 239 | output += non_tag_output 240 | continue 241 | assert isinstance(element, bs4.Tag), type(element) 242 | ... # Do whatever you want with the child 243 | ``` 244 | 245 | ##### Utility functions when overriding 246 | 247 | You may use (when extending) the following functions: 248 | 249 | - `__parse`, 2 parameters: 250 | - `html`: _bs4.BeautifulSoup_ 251 | - The html to unmark. This is used internally by the `unmark` method and is slightly faster. 252 | - `escape`: _bool_ 253 | - Whether to escape the characters inside the string or not. Defaults to `False`. 254 | - `escape`: 1 parameter: 255 | - `string`: _str_ 256 | - The string to escape and make markdown-safe 257 | - `wrap`: 2 parameters: 258 | - `element`: _bs4.BeautifulSoup_ 259 | - The element to wrap. 260 | - `around_with`: _str_ 261 | - The character to wrap the element around with. **WILL NOT BE ESCPAED** 262 | - And, of course, `tag_*` and `detect_language`. 263 | -------------------------------------------------------------------------------- /assets/benchmark.html: -------------------------------------------------------------------------------- 1 |

Sample Markdown

2 |

This is some basic, sample markdown.

3 |

Second Heading

4 |
    5 |
  • Unordered lists, and:
      6 |
    1. One
    2. 7 |
    3. Two
    4. 8 |
    5. Three
    6. 9 |
    10 |
  • 11 |
  • More
  • 12 |
13 |
14 |

Blockquote

15 |
16 |

And bold, italics, and even italics and later bold. Even strikethrough. A link to somewhere.

17 |

And code highlighting:

18 |
var foo = 'bar';
19 | 
20 | function baz(s) {
21 |    return foo + ':' + s;
22 | }
23 | 
24 |

Or inline code like var foo = 'bar';.

25 |

Or an image of bears

26 |

bears

27 |

The end ...

-------------------------------------------------------------------------------- /assets/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThatXliner/unmarkd/8c920c65a601a5c93ef3ab52edc46f2421827607/assets/benchmark.png -------------------------------------------------------------------------------- /assets/correct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThatXliner/unmarkd/8c920c65a601a5c93ef3ab52edc46f2421827607/assets/correct.png -------------------------------------------------------------------------------- /assets/tomd_cant_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThatXliner/unmarkd/8c920c65a601a5c93ef3ab52edc46f2421827607/assets/tomd_cant_handle.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "alabaster" 5 | version = "0.7.13" 6 | description = "A configurable sidebar-enabled Sphinx theme" 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, 11 | {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, 12 | ] 13 | 14 | [[package]] 15 | name = "attrs" 16 | version = "23.1.0" 17 | description = "Classes Without Boilerplate" 18 | optional = false 19 | python-versions = ">=3.7" 20 | files = [ 21 | {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, 22 | {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, 23 | ] 24 | 25 | [package.extras] 26 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 27 | dev = ["attrs[docs,tests]", "pre-commit"] 28 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 29 | tests = ["attrs[tests-no-zope]", "zope-interface"] 30 | tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 31 | 32 | [[package]] 33 | name = "autoflake" 34 | version = "2.1.1" 35 | description = "Removes unused imports and unused variables" 36 | optional = false 37 | python-versions = ">=3.7" 38 | files = [ 39 | {file = "autoflake-2.1.1-py3-none-any.whl", hash = "sha256:94e330a2bcf5ac01384fb2bf98bea60c6383eaa59ea62be486e376622deba985"}, 40 | {file = "autoflake-2.1.1.tar.gz", hash = "sha256:75524b48d42d6537041d91f17573b8a98cb645642f9f05c7fcc68de10b1cade3"}, 41 | ] 42 | 43 | [package.dependencies] 44 | pyflakes = ">=3.0.0" 45 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 46 | 47 | [[package]] 48 | name = "babel" 49 | version = "2.12.1" 50 | description = "Internationalization utilities" 51 | optional = false 52 | python-versions = ">=3.7" 53 | files = [ 54 | {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, 55 | {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, 56 | ] 57 | 58 | [package.dependencies] 59 | pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} 60 | 61 | [[package]] 62 | name = "beautifulsoup4" 63 | version = "4.12.2" 64 | description = "Screen-scraping library" 65 | optional = false 66 | python-versions = ">=3.6.0" 67 | files = [ 68 | {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, 69 | {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, 70 | ] 71 | 72 | [package.dependencies] 73 | soupsieve = ">1.2" 74 | 75 | [package.extras] 76 | html5lib = ["html5lib"] 77 | lxml = ["lxml"] 78 | 79 | [[package]] 80 | name = "black" 81 | version = "23.3.0" 82 | description = "The uncompromising code formatter." 83 | optional = false 84 | python-versions = ">=3.7" 85 | files = [ 86 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, 87 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, 88 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, 89 | {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, 90 | {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, 91 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, 92 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, 93 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, 94 | {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, 95 | {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, 96 | {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, 97 | {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, 98 | {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, 99 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, 100 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, 101 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, 102 | {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, 103 | {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, 104 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, 105 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, 106 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, 107 | {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, 108 | {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, 109 | {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, 110 | {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, 111 | ] 112 | 113 | [package.dependencies] 114 | click = ">=8.0.0" 115 | mypy-extensions = ">=0.4.3" 116 | packaging = ">=22.0" 117 | pathspec = ">=0.9.0" 118 | platformdirs = ">=2" 119 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 120 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 121 | 122 | [package.extras] 123 | colorama = ["colorama (>=0.4.3)"] 124 | d = ["aiohttp (>=3.7.4)"] 125 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 126 | uvloop = ["uvloop (>=0.15.2)"] 127 | 128 | [[package]] 129 | name = "certifi" 130 | version = "2023.5.7" 131 | description = "Python package for providing Mozilla's CA Bundle." 132 | optional = false 133 | python-versions = ">=3.6" 134 | files = [ 135 | {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, 136 | {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, 137 | ] 138 | 139 | [[package]] 140 | name = "charset-normalizer" 141 | version = "3.1.0" 142 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 143 | optional = false 144 | python-versions = ">=3.7.0" 145 | files = [ 146 | {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, 147 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, 148 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, 149 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, 150 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, 151 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, 152 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, 153 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, 154 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, 155 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, 156 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, 157 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, 158 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, 159 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, 160 | {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, 161 | {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, 162 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, 163 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, 164 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, 165 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, 166 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, 167 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, 168 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, 169 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, 170 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, 171 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, 172 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, 173 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, 174 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, 175 | {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, 176 | {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, 177 | {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, 178 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, 179 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, 180 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, 181 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, 182 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, 183 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, 184 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, 185 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, 186 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, 187 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, 188 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, 189 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, 190 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, 191 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, 192 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, 193 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, 194 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, 195 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, 196 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, 197 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, 198 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, 199 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, 200 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, 201 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, 202 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, 203 | {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, 204 | {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, 205 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, 206 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, 207 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, 208 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, 209 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, 210 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, 211 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, 212 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, 213 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, 214 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, 215 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, 216 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, 217 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, 218 | {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, 219 | {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, 220 | {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, 221 | ] 222 | 223 | [[package]] 224 | name = "click" 225 | version = "8.1.3" 226 | description = "Composable command line interface toolkit" 227 | optional = false 228 | python-versions = ">=3.7" 229 | files = [ 230 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 231 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 232 | ] 233 | 234 | [package.dependencies] 235 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 236 | 237 | [[package]] 238 | name = "colorama" 239 | version = "0.4.6" 240 | description = "Cross-platform colored terminal text." 241 | optional = false 242 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 243 | files = [ 244 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 245 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 246 | ] 247 | 248 | [[package]] 249 | name = "com2ann" 250 | version = "0.3.0" 251 | description = "Tool to translate type comments to annotations." 252 | optional = false 253 | python-versions = ">=3.8" 254 | files = [ 255 | {file = "com2ann-0.3.0-py3-none-any.whl", hash = "sha256:bb0994c7ea9e6f847c98b20d0cc056aa90c256937d3aede504026663d7f36bb6"}, 256 | {file = "com2ann-0.3.0.tar.gz", hash = "sha256:0da5e3900292057e5c4a5e33b21fe48817b4923437a095e6a677dff94b3d4e10"}, 257 | ] 258 | 259 | [[package]] 260 | name = "coverage" 261 | version = "7.2.7" 262 | description = "Code coverage measurement for Python" 263 | optional = false 264 | python-versions = ">=3.7" 265 | files = [ 266 | {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, 267 | {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, 268 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, 269 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, 270 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, 271 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, 272 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, 273 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, 274 | {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, 275 | {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, 276 | {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, 277 | {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, 278 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, 279 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, 280 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, 281 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, 282 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, 283 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, 284 | {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, 285 | {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, 286 | {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, 287 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, 288 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, 289 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, 290 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, 291 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, 292 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, 293 | {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, 294 | {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, 295 | {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, 296 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, 297 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, 298 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, 299 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, 300 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, 301 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, 302 | {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, 303 | {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, 304 | {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, 305 | {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, 306 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, 307 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, 308 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, 309 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, 310 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, 311 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, 312 | {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, 313 | {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, 314 | {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, 315 | {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, 316 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, 317 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, 318 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, 319 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, 320 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, 321 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, 322 | {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, 323 | {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, 324 | {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, 325 | {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, 326 | ] 327 | 328 | [package.dependencies] 329 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 330 | 331 | [package.extras] 332 | toml = ["tomli"] 333 | 334 | [[package]] 335 | name = "docutils" 336 | version = "0.19" 337 | description = "Docutils -- Python Documentation Utilities" 338 | optional = false 339 | python-versions = ">=3.7" 340 | files = [ 341 | {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, 342 | {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, 343 | ] 344 | 345 | [[package]] 346 | name = "exceptiongroup" 347 | version = "1.1.1" 348 | description = "Backport of PEP 654 (exception groups)" 349 | optional = false 350 | python-versions = ">=3.7" 351 | files = [ 352 | {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, 353 | {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, 354 | ] 355 | 356 | [package.extras] 357 | test = ["pytest (>=6)"] 358 | 359 | [[package]] 360 | name = "hypothesis" 361 | version = "6.76.0" 362 | description = "A library for property-based testing" 363 | optional = false 364 | python-versions = ">=3.7" 365 | files = [ 366 | {file = "hypothesis-6.76.0-py3-none-any.whl", hash = "sha256:034f73dd485933b0f4c319d7c3c58230492fdd7b16e821d67d150a78138adb93"}, 367 | {file = "hypothesis-6.76.0.tar.gz", hash = "sha256:526657eb3e4f2076b0383f722b2e6a92fd15d1d42db532decae8c41b14cab801"}, 368 | ] 369 | 370 | [package.dependencies] 371 | attrs = ">=19.2.0" 372 | exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 373 | sortedcontainers = ">=2.1.0,<3.0.0" 374 | 375 | [package.extras] 376 | all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.16.0)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2023.3)"] 377 | cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] 378 | codemods = ["libcst (>=0.3.16)"] 379 | dateutil = ["python-dateutil (>=1.4)"] 380 | django = ["django (>=3.2)"] 381 | dpcontracts = ["dpcontracts (>=0.4)"] 382 | ghostwriter = ["black (>=19.10b0)"] 383 | lark = ["lark (>=0.10.1)"] 384 | numpy = ["numpy (>=1.16.0)"] 385 | pandas = ["pandas (>=1.1)"] 386 | pytest = ["pytest (>=4.6)"] 387 | pytz = ["pytz (>=2014.1)"] 388 | redis = ["redis (>=3.0.0)"] 389 | zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2023.3)"] 390 | 391 | [[package]] 392 | name = "idna" 393 | version = "3.4" 394 | description = "Internationalized Domain Names in Applications (IDNA)" 395 | optional = false 396 | python-versions = ">=3.5" 397 | files = [ 398 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 399 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 400 | ] 401 | 402 | [[package]] 403 | name = "imagesize" 404 | version = "1.4.1" 405 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 406 | optional = false 407 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 408 | files = [ 409 | {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, 410 | {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, 411 | ] 412 | 413 | [[package]] 414 | name = "importlib-metadata" 415 | version = "6.6.0" 416 | description = "Read metadata from Python packages" 417 | optional = false 418 | python-versions = ">=3.7" 419 | files = [ 420 | {file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"}, 421 | {file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"}, 422 | ] 423 | 424 | [package.dependencies] 425 | zipp = ">=0.5" 426 | 427 | [package.extras] 428 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 429 | perf = ["ipython"] 430 | testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] 431 | 432 | [[package]] 433 | name = "iniconfig" 434 | version = "2.0.0" 435 | description = "brain-dead simple config-ini parsing" 436 | optional = false 437 | python-versions = ">=3.7" 438 | files = [ 439 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 440 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 441 | ] 442 | 443 | [[package]] 444 | name = "isort" 445 | version = "5.12.0" 446 | description = "A Python utility / library to sort Python imports." 447 | optional = false 448 | python-versions = ">=3.8.0" 449 | files = [ 450 | {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, 451 | {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, 452 | ] 453 | 454 | [package.extras] 455 | colors = ["colorama (>=0.4.3)"] 456 | pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] 457 | plugins = ["setuptools"] 458 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 459 | 460 | [[package]] 461 | name = "jinja2" 462 | version = "3.1.2" 463 | description = "A very fast and expressive template engine." 464 | optional = false 465 | python-versions = ">=3.7" 466 | files = [ 467 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, 468 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, 469 | ] 470 | 471 | [package.dependencies] 472 | MarkupSafe = ">=2.0" 473 | 474 | [package.extras] 475 | i18n = ["Babel (>=2.7)"] 476 | 477 | [[package]] 478 | name = "libcst" 479 | version = "1.0.0" 480 | description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." 481 | optional = false 482 | python-versions = ">=3.7" 483 | files = [ 484 | {file = "libcst-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb3d38d6e228df5cd3ce2e62917239f20c272f5821124cb2fc8ac93e661a16d5"}, 485 | {file = "libcst-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7d05877510001fab8843da8c523c2cee6b202f8cfe4da69389e53114cbc05a7"}, 486 | {file = "libcst-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ba3a3243b43d32f0a74dff42be1278d923853082da78feb8965138015686fa"}, 487 | {file = "libcst-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa948d82bd66188498dc241576fe660ff64007aa767ad50e2283d4a96f2b9d"}, 488 | {file = "libcst-1.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898a1e1d61efbc4a4658bab9af7fcb787d978fb9579ab2e37bf01121dfa03e84"}, 489 | {file = "libcst-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:715e75401f4a2aeb13606ee0eb5252f59ed423a30fb3d5f97f02a1a01565cab0"}, 490 | {file = "libcst-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e4a24daa7d71e2667d7a1db96104e1d2be22445bc383cdd305175344df4e3e10"}, 491 | {file = "libcst-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d685bbdd666e17e844cd1f452987c59f51093a6e22ce17af74395426a9588d81"}, 492 | {file = "libcst-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d5e1569660b73260f1b2c9910daab6a29a6597d46873a548efafcf625495a6f"}, 493 | {file = "libcst-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bfba66b6d049e2f5266e7d2fefd99d90b90a7f5270904a15ec176f206495a7a"}, 494 | {file = "libcst-1.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00439f1d7d800248d9f249154f9103d32524bdd3bddc3a28ae7e2544f0d0638b"}, 495 | {file = "libcst-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:d90f8abad09a94a5dfb386d730df5db54e8e095665451bca780b0cb91787b522"}, 496 | {file = "libcst-1.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9e7319193589fc7e34d16d9dfb1509f273b1a0b685203294e3dc43448da545b4"}, 497 | {file = "libcst-1.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c500c5d65221d161bbf9dba917c8d06d95800d85a94914638a59e4f7ebf52c96"}, 498 | {file = "libcst-1.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37149c7d35163290a2d3fd029735110beaae5e068e6799ed54860b9546048e30"}, 499 | {file = "libcst-1.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:778a54edb6a785e0c78a727287017f0200ce2fd994246d1017a72a51de4e0677"}, 500 | {file = "libcst-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3d3468e4823f51bcf84b14aac47e23938a5ad579d65e8a5ebb30078f0416f205"}, 501 | {file = "libcst-1.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a38f5071cc38578e09a80f96c9e1a20fe7b78d3cc15d97e562d261076a763195"}, 502 | {file = "libcst-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:823972ed0c6197dcd02940bdda016224534fa4517e651742f8430a46f9a200a3"}, 503 | {file = "libcst-1.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b45cbf0abf9c07b969e3eda0183486fc2439bef6e1bf88fe68dd31b930f727"}, 504 | {file = "libcst-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:393263e80ae1df4fdc04c6df0019715c5bab1d8add4eed434c699951cf31293d"}, 505 | {file = "libcst-1.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9168899f5f1476c426f2ec5c5f66cb643503ac244bcd70feb74f81499d81d58"}, 506 | {file = "libcst-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:4892a84bd9c5a62239de326fc86234b2913a0d1ace391040be8443be05bbc642"}, 507 | {file = "libcst-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e41ff5f24e59db5be08b85817c8bae77d37d56c6d999125bfa17daccf0dbbef8"}, 508 | {file = "libcst-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b2b914804c0ec03446293139b8d00d5ee4f5d95d1f98b06a27e86e76f3e9020"}, 509 | {file = "libcst-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b45eade33b0870debf2a7fade2f28cc34c8f99d192ca59e064c45d9f8763c70"}, 510 | {file = "libcst-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:221c2bc5a6b6ed431b31c47d4de8ae6a8224bfbf161601c47abead1084b47271"}, 511 | {file = "libcst-1.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7988cfeacbdff652774e9ccee5f25a418487733d98b13d38c4c4e4ba5d7a16ff"}, 512 | {file = "libcst-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:57de502a8fd84bf086732c72303f51dcece9b9f114bd64225e4c928b34a67b19"}, 513 | {file = "libcst-1.0.0.tar.gz", hash = "sha256:aaf3471c11a7dfc76c46d0b80e64bde5d066d88393fe14b79bcaeb6753744bae"}, 514 | ] 515 | 516 | [package.dependencies] 517 | pyyaml = ">=5.2" 518 | typing-extensions = ">=3.7.4.2" 519 | typing-inspect = ">=0.4.0" 520 | 521 | [package.extras] 522 | dev = ["Sphinx (>=5.1.1)", "black (==23.1.0)", "build (>=0.10.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8,<5)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.2)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.14)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.10)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.1.0)", "usort (==1.0.6)"] 523 | 524 | [[package]] 525 | name = "markdown-it-py" 526 | version = "2.2.0" 527 | description = "Python port of markdown-it. Markdown parsing, done right!" 528 | optional = false 529 | python-versions = ">=3.7" 530 | files = [ 531 | {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, 532 | {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, 533 | ] 534 | 535 | [package.dependencies] 536 | mdurl = ">=0.1,<1.0" 537 | 538 | [package.extras] 539 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 540 | code-style = ["pre-commit (>=3.0,<4.0)"] 541 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 542 | linkify = ["linkify-it-py (>=1,<3)"] 543 | plugins = ["mdit-py-plugins"] 544 | profiling = ["gprof2dot"] 545 | rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 546 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 547 | 548 | [[package]] 549 | name = "marko" 550 | version = "1.3.0" 551 | description = "A markdown parser with high extensibility." 552 | optional = false 553 | python-versions = ">=3.7" 554 | files = [ 555 | {file = "marko-1.3.0-py3-none-any.whl", hash = "sha256:f5b03e68fa2fb76810d5e51a198d343e3b35e028d45e43c543c7c45e82170b66"}, 556 | {file = "marko-1.3.0.tar.gz", hash = "sha256:8e52155860ebfb0394f92bc1ca7e5a30e4fca5392d71bf2af2b11971e8972667"}, 557 | ] 558 | 559 | [package.extras] 560 | codehilite = ["pygments"] 561 | toc = ["python-slugify"] 562 | 563 | [[package]] 564 | name = "markupsafe" 565 | version = "2.1.3" 566 | description = "Safely add untrusted strings to HTML/XML markup." 567 | optional = false 568 | python-versions = ">=3.7" 569 | files = [ 570 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, 571 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, 572 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, 573 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, 574 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, 575 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, 576 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, 577 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, 578 | {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, 579 | {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, 580 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, 581 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, 582 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, 583 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, 584 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, 585 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, 586 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, 587 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, 588 | {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, 589 | {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, 590 | {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, 591 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, 592 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, 593 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, 594 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, 595 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, 596 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, 597 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, 598 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, 599 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, 600 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, 601 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, 602 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, 603 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, 604 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, 605 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, 606 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, 607 | {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, 608 | {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, 609 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, 610 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, 611 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, 612 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, 613 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, 614 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, 615 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, 616 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, 617 | {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, 618 | {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, 619 | {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, 620 | ] 621 | 622 | [[package]] 623 | name = "mdurl" 624 | version = "0.1.2" 625 | description = "Markdown URL utilities" 626 | optional = false 627 | python-versions = ">=3.7" 628 | files = [ 629 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 630 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 631 | ] 632 | 633 | [[package]] 634 | name = "mypy" 635 | version = "1.3.0" 636 | description = "Optional static typing for Python" 637 | optional = false 638 | python-versions = ">=3.7" 639 | files = [ 640 | {file = "mypy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d"}, 641 | {file = "mypy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85"}, 642 | {file = "mypy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd"}, 643 | {file = "mypy-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152"}, 644 | {file = "mypy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228"}, 645 | {file = "mypy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd"}, 646 | {file = "mypy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c"}, 647 | {file = "mypy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae"}, 648 | {file = "mypy-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca"}, 649 | {file = "mypy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf"}, 650 | {file = "mypy-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409"}, 651 | {file = "mypy-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929"}, 652 | {file = "mypy-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"}, 653 | {file = "mypy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee"}, 654 | {file = "mypy-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f"}, 655 | {file = "mypy-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb"}, 656 | {file = "mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4"}, 657 | {file = "mypy-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305"}, 658 | {file = "mypy-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf"}, 659 | {file = "mypy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8"}, 660 | {file = "mypy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703"}, 661 | {file = "mypy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017"}, 662 | {file = "mypy-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e"}, 663 | {file = "mypy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a"}, 664 | {file = "mypy-1.3.0-py3-none-any.whl", hash = "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897"}, 665 | {file = "mypy-1.3.0.tar.gz", hash = "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11"}, 666 | ] 667 | 668 | [package.dependencies] 669 | mypy-extensions = ">=1.0.0" 670 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 671 | typing-extensions = ">=3.10" 672 | 673 | [package.extras] 674 | dmypy = ["psutil (>=4.0)"] 675 | install-types = ["pip"] 676 | python2 = ["typed-ast (>=1.4.0,<2)"] 677 | reports = ["lxml"] 678 | 679 | [[package]] 680 | name = "mypy-extensions" 681 | version = "1.0.0" 682 | description = "Type system extensions for programs checked with the mypy type checker." 683 | optional = false 684 | python-versions = ">=3.5" 685 | files = [ 686 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 687 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 688 | ] 689 | 690 | [[package]] 691 | name = "packaging" 692 | version = "23.1" 693 | description = "Core utilities for Python packages" 694 | optional = false 695 | python-versions = ">=3.7" 696 | files = [ 697 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 698 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 699 | ] 700 | 701 | [[package]] 702 | name = "pathspec" 703 | version = "0.11.1" 704 | description = "Utility library for gitignore style pattern matching of file paths." 705 | optional = false 706 | python-versions = ">=3.7" 707 | files = [ 708 | {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, 709 | {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, 710 | ] 711 | 712 | [[package]] 713 | name = "platformdirs" 714 | version = "3.5.1" 715 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 716 | optional = false 717 | python-versions = ">=3.7" 718 | files = [ 719 | {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, 720 | {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, 721 | ] 722 | 723 | [package.extras] 724 | docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] 725 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 726 | 727 | [[package]] 728 | name = "pluggy" 729 | version = "1.0.0" 730 | description = "plugin and hook calling mechanisms for python" 731 | optional = false 732 | python-versions = ">=3.6" 733 | files = [ 734 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 735 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 736 | ] 737 | 738 | [package.extras] 739 | dev = ["pre-commit", "tox"] 740 | testing = ["pytest", "pytest-benchmark"] 741 | 742 | [[package]] 743 | name = "pprintpp" 744 | version = "0.4.0" 745 | description = "A drop-in replacement for pprint that's actually pretty" 746 | optional = false 747 | python-versions = "*" 748 | files = [ 749 | {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, 750 | {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, 751 | ] 752 | 753 | [[package]] 754 | name = "pyflakes" 755 | version = "3.0.1" 756 | description = "passive checker of Python programs" 757 | optional = false 758 | python-versions = ">=3.6" 759 | files = [ 760 | {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, 761 | {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, 762 | ] 763 | 764 | [[package]] 765 | name = "pygments" 766 | version = "2.15.1" 767 | description = "Pygments is a syntax highlighting package written in Python." 768 | optional = false 769 | python-versions = ">=3.7" 770 | files = [ 771 | {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, 772 | {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, 773 | ] 774 | 775 | [package.extras] 776 | plugins = ["importlib-metadata"] 777 | 778 | [[package]] 779 | name = "pytest" 780 | version = "7.3.1" 781 | description = "pytest: simple powerful testing with Python" 782 | optional = false 783 | python-versions = ">=3.7" 784 | files = [ 785 | {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, 786 | {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, 787 | ] 788 | 789 | [package.dependencies] 790 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 791 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 792 | iniconfig = "*" 793 | packaging = "*" 794 | pluggy = ">=0.12,<2.0" 795 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 796 | 797 | [package.extras] 798 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 799 | 800 | [[package]] 801 | name = "pytest-clarity" 802 | version = "1.0.1" 803 | description = "A plugin providing an alternative, colourful diff output for failing assertions." 804 | optional = false 805 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 806 | files = [ 807 | {file = "pytest-clarity-1.0.1.tar.gz", hash = "sha256:505fe345fad4fe11c6a4187fe683f2c7c52c077caa1e135f3e483fe112db7772"}, 808 | ] 809 | 810 | [package.dependencies] 811 | pprintpp = ">=0.4.0" 812 | pytest = ">=3.5.0" 813 | rich = ">=8.0.0" 814 | 815 | [[package]] 816 | name = "pytest-cov" 817 | version = "4.1.0" 818 | description = "Pytest plugin for measuring coverage." 819 | optional = false 820 | python-versions = ">=3.7" 821 | files = [ 822 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 823 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 824 | ] 825 | 826 | [package.dependencies] 827 | coverage = {version = ">=5.2.1", extras = ["toml"]} 828 | pytest = ">=4.6" 829 | 830 | [package.extras] 831 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 832 | 833 | [[package]] 834 | name = "pytz" 835 | version = "2023.3" 836 | description = "World timezone definitions, modern and historical" 837 | optional = false 838 | python-versions = "*" 839 | files = [ 840 | {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, 841 | {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, 842 | ] 843 | 844 | [[package]] 845 | name = "pyupgrade" 846 | version = "3.4.0" 847 | description = "A tool to automatically upgrade syntax for newer versions." 848 | optional = false 849 | python-versions = ">=3.8" 850 | files = [ 851 | {file = "pyupgrade-3.4.0-py2.py3-none-any.whl", hash = "sha256:98b6bee32149f662da1aa038cb9baf93d592ae696059acd613db6ff583583048"}, 852 | {file = "pyupgrade-3.4.0.tar.gz", hash = "sha256:f6bcf0d5e59170d178a2630e981d0e7b04d9b13f3c0e7e62f3e6bab582f841e4"}, 853 | ] 854 | 855 | [package.dependencies] 856 | tokenize-rt = ">=3.2.0" 857 | 858 | [[package]] 859 | name = "pyyaml" 860 | version = "6.0" 861 | description = "YAML parser and emitter for Python" 862 | optional = false 863 | python-versions = ">=3.6" 864 | files = [ 865 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 866 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 867 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 868 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 869 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 870 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 871 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 872 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 873 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 874 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 875 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 876 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 877 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 878 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 879 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 880 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 881 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 882 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 883 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 884 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 885 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 886 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 887 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 888 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 889 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 890 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 891 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 892 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 893 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 894 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 895 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 896 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 897 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 898 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 899 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 900 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 901 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 902 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 903 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 904 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 905 | ] 906 | 907 | [[package]] 908 | name = "requests" 909 | version = "2.31.0" 910 | description = "Python HTTP for Humans." 911 | optional = false 912 | python-versions = ">=3.7" 913 | files = [ 914 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 915 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 916 | ] 917 | 918 | [package.dependencies] 919 | certifi = ">=2017.4.17" 920 | charset-normalizer = ">=2,<4" 921 | idna = ">=2.5,<4" 922 | urllib3 = ">=1.21.1,<3" 923 | 924 | [package.extras] 925 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 926 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 927 | 928 | [[package]] 929 | name = "rich" 930 | version = "13.4.1" 931 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 932 | optional = false 933 | python-versions = ">=3.7.0" 934 | files = [ 935 | {file = "rich-13.4.1-py3-none-any.whl", hash = "sha256:d204aadb50b936bf6b1a695385429d192bc1fdaf3e8b907e8e26f4c4e4b5bf75"}, 936 | {file = "rich-13.4.1.tar.gz", hash = "sha256:76f6b65ea7e5c5d924ba80e322231d7cb5b5981aa60bfc1e694f1bc097fe6fe1"}, 937 | ] 938 | 939 | [package.dependencies] 940 | markdown-it-py = ">=2.2.0,<3.0.0" 941 | pygments = ">=2.13.0,<3.0.0" 942 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} 943 | 944 | [package.extras] 945 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 946 | 947 | [[package]] 948 | name = "ruff" 949 | version = "0.0.249" 950 | description = "An extremely fast Python linter, written in Rust." 951 | optional = false 952 | python-versions = ">=3.7" 953 | files = [ 954 | {file = "ruff-0.0.249-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:03a26f1cb5605508de49d921d0970895b9e3ad4021f776a53be18fa95a4fc25b"}, 955 | {file = "ruff-0.0.249-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:46537d960221e97adc6a3556159ab3ae4b722b9985de13c50b436732d4659af0"}, 956 | {file = "ruff-0.0.249-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2dcc1f3053092aeedef8e47704e301b74687fa480fe5e7ebef2b0eb2e4a0bd"}, 957 | {file = "ruff-0.0.249-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:855cfe47d146a1eb68347025c7b5ad651c083343de6cb7ccf90585bda3e381db"}, 958 | {file = "ruff-0.0.249-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf3af16748c8539a48451edbcb687994eccc6a764c95f42de22195007ae13a24"}, 959 | {file = "ruff-0.0.249-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2815e05ba168dee6708dbbdab8d0c145bb3b0085c91ee552839c1c18a52f6cb1"}, 960 | {file = "ruff-0.0.249-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ab43389216cc8403db84992977e6f5e8fee83bd10aca05e1f2f262754cd8384"}, 961 | {file = "ruff-0.0.249-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b67c44ab260d3a838ec237c7234be1098bf2ef1421036fbbb229698513d1fc3"}, 962 | {file = "ruff-0.0.249-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d859744e1cc95ad5e52c4642509b3abb5ea0833f0529c380c2731b8cab5726"}, 963 | {file = "ruff-0.0.249-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6f494276ee281eb09c7026cc17df1bfc2fe59ab39a87196014ce093ff27f1a0"}, 964 | {file = "ruff-0.0.249-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:be23c57b9551d8fcf559755e5bc56ac5bcbc3215fc8a3190ea6ed1bb9133d8dd"}, 965 | {file = "ruff-0.0.249-py3-none-musllinux_1_2_i686.whl", hash = "sha256:980a3bce8ba38c9b47bc000915e80a672add9f7e9c5b128375486ec8cd8f860d"}, 966 | {file = "ruff-0.0.249-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f1e988e9365b11c6d7796c0d4a0556f6a26f0627fe57e9e7411ff91f421fb502"}, 967 | {file = "ruff-0.0.249-py3-none-win32.whl", hash = "sha256:f4837a7e6d1ff81cb027695deb28793e0945cca8d88e87b46ff845ef38d52c82"}, 968 | {file = "ruff-0.0.249-py3-none-win_amd64.whl", hash = "sha256:4cc437ab55a35088008dbe9db598cd8e240b5f70fb88eb8ab6fa1de529007f30"}, 969 | {file = "ruff-0.0.249-py3-none-win_arm64.whl", hash = "sha256:3d2d11a7b750433f3acec30641faab673d101aa86a2ddfe4af8bcfa773b178e2"}, 970 | {file = "ruff-0.0.249.tar.gz", hash = "sha256:b590689f08ecef971c45555cbda6854cdf48f3828fc326802828e851b1a14b3d"}, 971 | ] 972 | 973 | [[package]] 974 | name = "shed" 975 | version = "0.10.9" 976 | description = "`shed` canonicalises Python code." 977 | optional = false 978 | python-versions = ">=3.7" 979 | files = [ 980 | {file = "shed-0.10.9-py3-none-any.whl", hash = "sha256:ca2c4863abba47df2463fed51bca1cc81b4380b0fb07e18ccc207506ea5ef03b"}, 981 | {file = "shed-0.10.9.tar.gz", hash = "sha256:586409073267c0b74ec82526d0bcd1f9e857f785d9f1f07e6c3f3f1091e82712"}, 982 | ] 983 | 984 | [package.dependencies] 985 | autoflake = ">=1.4" 986 | black = ">=23.1.0" 987 | com2ann = {version = ">=0.3.0", markers = "python_version >= \"3.8\""} 988 | isort = ">=5.10.1" 989 | libcst = ">=0.4.7" 990 | pyupgrade = ">=3.0.0" 991 | 992 | [[package]] 993 | name = "snowballstemmer" 994 | version = "2.2.0" 995 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 996 | optional = false 997 | python-versions = "*" 998 | files = [ 999 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 1000 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "sortedcontainers" 1005 | version = "2.4.0" 1006 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 1007 | optional = false 1008 | python-versions = "*" 1009 | files = [ 1010 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 1011 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "soupsieve" 1016 | version = "2.4.1" 1017 | description = "A modern CSS selector implementation for Beautiful Soup." 1018 | optional = false 1019 | python-versions = ">=3.7" 1020 | files = [ 1021 | {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, 1022 | {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "sphinx" 1027 | version = "6.2.1" 1028 | description = "Python documentation generator" 1029 | optional = false 1030 | python-versions = ">=3.8" 1031 | files = [ 1032 | {file = "Sphinx-6.2.1.tar.gz", hash = "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b"}, 1033 | {file = "sphinx-6.2.1-py3-none-any.whl", hash = "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912"}, 1034 | ] 1035 | 1036 | [package.dependencies] 1037 | alabaster = ">=0.7,<0.8" 1038 | babel = ">=2.9" 1039 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 1040 | docutils = ">=0.18.1,<0.20" 1041 | imagesize = ">=1.3" 1042 | importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} 1043 | Jinja2 = ">=3.0" 1044 | packaging = ">=21.0" 1045 | Pygments = ">=2.13" 1046 | requests = ">=2.25.0" 1047 | snowballstemmer = ">=2.0" 1048 | sphinxcontrib-applehelp = "*" 1049 | sphinxcontrib-devhelp = "*" 1050 | sphinxcontrib-htmlhelp = ">=2.0.0" 1051 | sphinxcontrib-jsmath = "*" 1052 | sphinxcontrib-qthelp = "*" 1053 | sphinxcontrib-serializinghtml = ">=1.1.5" 1054 | 1055 | [package.extras] 1056 | docs = ["sphinxcontrib-websupport"] 1057 | lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] 1058 | test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] 1059 | 1060 | [[package]] 1061 | name = "sphinxcontrib-applehelp" 1062 | version = "1.0.4" 1063 | description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" 1064 | optional = false 1065 | python-versions = ">=3.8" 1066 | files = [ 1067 | {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, 1068 | {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, 1069 | ] 1070 | 1071 | [package.extras] 1072 | lint = ["docutils-stubs", "flake8", "mypy"] 1073 | test = ["pytest"] 1074 | 1075 | [[package]] 1076 | name = "sphinxcontrib-devhelp" 1077 | version = "1.0.2" 1078 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." 1079 | optional = false 1080 | python-versions = ">=3.5" 1081 | files = [ 1082 | {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, 1083 | {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, 1084 | ] 1085 | 1086 | [package.extras] 1087 | lint = ["docutils-stubs", "flake8", "mypy"] 1088 | test = ["pytest"] 1089 | 1090 | [[package]] 1091 | name = "sphinxcontrib-htmlhelp" 1092 | version = "2.0.1" 1093 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 1094 | optional = false 1095 | python-versions = ">=3.8" 1096 | files = [ 1097 | {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, 1098 | {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, 1099 | ] 1100 | 1101 | [package.extras] 1102 | lint = ["docutils-stubs", "flake8", "mypy"] 1103 | test = ["html5lib", "pytest"] 1104 | 1105 | [[package]] 1106 | name = "sphinxcontrib-jsmath" 1107 | version = "1.0.1" 1108 | description = "A sphinx extension which renders display math in HTML via JavaScript" 1109 | optional = false 1110 | python-versions = ">=3.5" 1111 | files = [ 1112 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 1113 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 1114 | ] 1115 | 1116 | [package.extras] 1117 | test = ["flake8", "mypy", "pytest"] 1118 | 1119 | [[package]] 1120 | name = "sphinxcontrib-qthelp" 1121 | version = "1.0.3" 1122 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." 1123 | optional = false 1124 | python-versions = ">=3.5" 1125 | files = [ 1126 | {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, 1127 | {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, 1128 | ] 1129 | 1130 | [package.extras] 1131 | lint = ["docutils-stubs", "flake8", "mypy"] 1132 | test = ["pytest"] 1133 | 1134 | [[package]] 1135 | name = "sphinxcontrib-serializinghtml" 1136 | version = "1.1.5" 1137 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." 1138 | optional = false 1139 | python-versions = ">=3.5" 1140 | files = [ 1141 | {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, 1142 | {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, 1143 | ] 1144 | 1145 | [package.extras] 1146 | lint = ["docutils-stubs", "flake8", "mypy"] 1147 | test = ["pytest"] 1148 | 1149 | [[package]] 1150 | name = "tokenize-rt" 1151 | version = "5.0.0" 1152 | description = "A wrapper around the stdlib `tokenize` which roundtrips." 1153 | optional = false 1154 | python-versions = ">=3.7" 1155 | files = [ 1156 | {file = "tokenize_rt-5.0.0-py2.py3-none-any.whl", hash = "sha256:c67772c662c6b3dc65edf66808577968fb10badfc2042e3027196bed4daf9e5a"}, 1157 | {file = "tokenize_rt-5.0.0.tar.gz", hash = "sha256:3160bc0c3e8491312d0485171dea861fc160a240f5f5766b72a1165408d10740"}, 1158 | ] 1159 | 1160 | [[package]] 1161 | name = "toml" 1162 | version = "0.10.2" 1163 | description = "Python Library for Tom's Obvious, Minimal Language" 1164 | optional = false 1165 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 1166 | files = [ 1167 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1168 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "tomli" 1173 | version = "2.0.1" 1174 | description = "A lil' TOML parser" 1175 | optional = false 1176 | python-versions = ">=3.7" 1177 | files = [ 1178 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1179 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "types-beautifulsoup4" 1184 | version = "4.12.0.5" 1185 | description = "Typing stubs for beautifulsoup4" 1186 | optional = false 1187 | python-versions = "*" 1188 | files = [ 1189 | {file = "types-beautifulsoup4-4.12.0.5.tar.gz", hash = "sha256:d9be456416a62a5b9740559592e1063a69d4b0a1b83911d878558c8ae8e07074"}, 1190 | {file = "types_beautifulsoup4-4.12.0.5-py3-none-any.whl", hash = "sha256:718364c8e6787884501c700b1d22b6c0a8711bf9d6c9bf96e1fba81495bc46a8"}, 1191 | ] 1192 | 1193 | [package.dependencies] 1194 | types-html5lib = "*" 1195 | 1196 | [[package]] 1197 | name = "types-html5lib" 1198 | version = "1.1.11.14" 1199 | description = "Typing stubs for html5lib" 1200 | optional = false 1201 | python-versions = "*" 1202 | files = [ 1203 | {file = "types-html5lib-1.1.11.14.tar.gz", hash = "sha256:091e9e74e0ee37c93fd789a164e99b2af80ecf5a314280450c6a763d027ea209"}, 1204 | {file = "types_html5lib-1.1.11.14-py3-none-any.whl", hash = "sha256:758c1a27f3b63363a346f3646be9f8b1f25df4fc1f96f88af6d1d831f24ad675"}, 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "typing-extensions" 1209 | version = "4.6.3" 1210 | description = "Backported and Experimental Type Hints for Python 3.7+" 1211 | optional = false 1212 | python-versions = ">=3.7" 1213 | files = [ 1214 | {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"}, 1215 | {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"}, 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "typing-inspect" 1220 | version = "0.9.0" 1221 | description = "Runtime inspection utilities for typing module." 1222 | optional = false 1223 | python-versions = "*" 1224 | files = [ 1225 | {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, 1226 | {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, 1227 | ] 1228 | 1229 | [package.dependencies] 1230 | mypy-extensions = ">=0.3.0" 1231 | typing-extensions = ">=3.7.4" 1232 | 1233 | [[package]] 1234 | name = "urllib3" 1235 | version = "2.0.2" 1236 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1237 | optional = false 1238 | python-versions = ">=3.7" 1239 | files = [ 1240 | {file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, 1241 | {file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, 1242 | ] 1243 | 1244 | [package.extras] 1245 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1246 | secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] 1247 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1248 | zstd = ["zstandard (>=0.18.0)"] 1249 | 1250 | [[package]] 1251 | name = "zipp" 1252 | version = "3.15.0" 1253 | description = "Backport of pathlib-compatible object wrapper for zip files" 1254 | optional = false 1255 | python-versions = ">=3.7" 1256 | files = [ 1257 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 1258 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 1259 | ] 1260 | 1261 | [package.extras] 1262 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 1263 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 1264 | 1265 | [metadata] 1266 | lock-version = "2.0" 1267 | python-versions = "^3.8" 1268 | content-hash = "ef0695f65d68909bd5bd9e3b6ac26f314743e9ab491569b1e3192c647d7f8863" 1269 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "unmarkd" 3 | description = "A markdown reverser" 4 | authors = ["Bryan Hu "] 5 | version = "1.1.3" 6 | 7 | readme = "README.md" 8 | license = "GPL-3.0-or-later" 9 | classifiers = [ 10 | # Get the list of trove classifiers here: https://pypi.org/classifiers/ 11 | "Programming Language :: Python :: Implementation :: CPython", 12 | "Operating System :: OS Independent", 13 | "Typing :: Typed", 14 | "Topic :: Text Processing :: Markup :: Markdown", 15 | "Topic :: Text Processing :: Markup :: HTML", 16 | "Topic :: Software Development :: Libraries :: Python Modules", 17 | "Development Status :: 5 - Production/Stable", 18 | "Natural Language :: English", 19 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 20 | ] 21 | documentation = "https://unmarkd.readthedocs.io/en/latest/index.html" 22 | homepage = "https://github.com/ThatXliner/unmarkd" 23 | # keywords = ["awesome", "project"] # Maximum of 5 keywords 24 | 25 | [tool.poetry.dependencies] 26 | python = "^3.8" 27 | beautifulsoup4 = ">=4.12.2" 28 | 29 | [tool.poetry.group.dev.dependencies] 30 | black = "^23.1.0" 31 | hypothesis = "^6.68.2" 32 | mypy = "^1.0.1" 33 | pytest = "^7.2.1" 34 | pytest-clarity = "^1.0.1" 35 | pytest-cov = "^4.0.0" 36 | ruff = "^0.0.249" 37 | shed = "^0.10.9" 38 | 39 | Sphinx = "^6.1.3" 40 | toml = "^0.10.2" 41 | types-beautifulsoup4 = "^4.12.0.5" 42 | marko = "^1.3.0" 43 | 44 | [tool.poe.tasks] 45 | # Code linting 46 | mypy = {cmd = "mypy unmarkd --strict", help = "Run MyPy on codebase"} 47 | ruff = {cmd = "ruff check unmarkd", help = "Run Ruff on codebase"} 48 | check_black = {cmd = "black unmarkd --check"} 49 | check_imports = {cmd = "ruff check unmarkd --select I"} 50 | style = ["check_black", "check_imports"] 51 | codebase = ["ruff", "mypy"] 52 | [tool.poe.tasks.docs] 53 | cmd = "sphinx-build docs build" 54 | help = "Build documentation" 55 | 56 | [tool.poe.tasks.format] 57 | cmd = "shed" 58 | help = "Format code" 59 | 60 | [tool.poe.tasks.fix-ruff] 61 | cmd = "ruff unmarkd --fix" 62 | help = "Ruff autofix" 63 | 64 | [tool.poe.tasks.lint] 65 | sequence = ["style", "codebase"] 66 | help = "Lint codebase" 67 | 68 | [tool.poe.tasks.test] 69 | cmd = "pytest -vvv --cov=unmarkd" 70 | help = "Simply run test suite" 71 | 72 | [tool.poe.tasks.ci] 73 | cmd = "pytest -vvv --cov=unmarkd --cov-report=xml" 74 | help = "This workflow is for Github Actions" 75 | 76 | 77 | [build-system] 78 | requires = ["poetry-core>=1.0.0"] 79 | build-backend = "poetry.core.masonry.api" 80 | 81 | [tool.ruff] 82 | # Same as Black. 83 | line-length = 88 84 | select = ["ALL"] 85 | ignore = [ 86 | # "D", # "No docs" 87 | "T20", # "Don't use print or pprint" 88 | # "ANN", # Type annotation errors (or the lack of it) 89 | "FBT", # I actually don't know why this exists 90 | # and it seems useless so ignore it 91 | # Fix doc rule conflicts 92 | "D203", 93 | "D213" 94 | ] 95 | target-version = "py38" 96 | [tool.ruff.per-file-ignores] 97 | "tests/**/*.py" = ["S101", "ANN101", "D"] 98 | "tests/test_roundtrip.py" = ["E501", "ANN001"] 99 | "docs/conf.py" = ["INP001", "A001"] 100 | "unmarkd/unmarkers.py" = ["D102", "S101"] 101 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThatXliner/unmarkd/8c920c65a601a5c93ef3ab52edc46f2421827607/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_edgecases.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import unmarkd 3 | 4 | 5 | class TestComments: 6 | def test_bare(self) -> None: 7 | assert unmarkd.unmark("") == "" 8 | 9 | def test_list(self) -> None: 10 | assert ( 11 | unmarkd.unmark("\n
  1. Hi
") 12 | == "\n\n1. Hi" 13 | ) 14 | assert ( 15 | unmarkd.unmark("
    \n
  1. Hi
") 16 | == "\n1. Hi" 17 | ), unmarkd.unmark("
    \n
  1. Hi
") 18 | 19 | 20 | def test_empty_html() -> None: 21 | assert unmarkd.unmark(BeautifulSoup("")) == "" 22 | 23 | 24 | def test_custom_elements() -> None: 25 | assert ( 26 | unmarkd.unmark("") 27 | == "" 28 | ) 29 | 30 | 31 | def test_header_tag() -> None: 32 | assert unmarkd.unmark("
yo
") == "
yo
" 33 | -------------------------------------------------------------------------------- /tests/test_roundtrip.py: -------------------------------------------------------------------------------- 1 | import unicodedata 2 | 3 | import marko 4 | from hypothesis import assume, given 5 | from hypothesis import strategies as st 6 | 7 | import unmarkd 8 | 9 | 10 | def helper(text: str, func=unmarkd.unmark) -> None: 11 | value0 = marko.convert(text) 12 | unmarked = func(html=value0) 13 | value1 = marko.convert(unmarked) 14 | assert value0 == value1, (value0, value1, unmarked) 15 | 16 | 17 | @given(text=st.text(st.characters(blacklist_categories=("Cc", "Cf", "Cs", "Co", "Cn")))) 18 | def test_roundtrip_commonmark_unmark(text: str) -> None: 19 | assume(unicodedata.normalize("NFKC", text).strip() == text) 20 | helper(text) 21 | helper(text, unmarkd.unmarkers.StackOverflowUnmarker().unmark) 22 | 23 | 24 | # fmt: off 25 | class TestExampleCases: 26 | # pylint: disable=C0116,R0201,C0321 27 | def test_example_1(self) -> None: helper("") 28 | def test_example_2(self) -> None: helper("` `") 29 | def test_example_3(self) -> None: helper("0\n\n0") 30 | def test_example_4(self) -> None: helper("```\n```") 31 | def test_example_5(self) -> None: helper("0.") 32 | def test_example_6(self) -> None: helper("```") 33 | def test_example_7(self) -> None: helper(R"*\**") 34 | def test_example_8(self) -> None: helper(R"**\***") 35 | def test_example_9(self) -> None: helper(R"`\``") 36 | def test_example_10(self) -> None: helper(R"-") 37 | def test_example_11(self) -> None: helper(R">") 38 | def test_example_12(self) -> None: helper(R"<") 39 | def test_example_13(self) -> None: helper("*foo `bar* baz`") 40 | def test_example_14(self) -> None: helper(">>") 41 | def test_example_15(self) -> None: helper(R"~~\~~~") 42 | def test_example_16(self) -> None: helper("[link](https://github.com 'GitHub Homepage')") 43 | def test_example_17(self) -> None: helper("[link](https://github.com)") 44 | def test_example_18(self) -> None: helper("![img](https://github.com)") 45 | def test_example_19(self) -> None: helper("""# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6""") 46 | def test_example_20(self) -> None: helper("~~Nothing.~~") 47 | def test_example_21(self) -> None: helper("```0") 48 | def test_example_22(self) -> None: helper("- Unordered lists, and:\n 1. One\n 2. Two\n 3. Three\n- More") 49 | def test_example_23(self) -> None: helper("a\n---\nb") 50 | def test_example_24(self) -> None: assert unmarkd.unmark("""

A

""") == "A" 51 | def test_example_25(self) -> None: helper(">>>") 52 | def test_example_26(self) -> None: helper(R"\#") 53 | def test_example_27(self) -> None: assert unmarkd.unmark("
    \n
  1. A
  2. \n
  3. B
  4. \n
  5. C
") == "1. A\n2. B\n3. **C**" 54 | def test_example_28(self) -> None: helper("~~*e*~~ and ~~**b**~~") 55 | # fmt: on 56 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import toml 4 | 5 | from unmarkd import __version__ 6 | 7 | project_dir = Path(__file__).parent.parent 8 | 9 | 10 | def test_version() -> None: 11 | assert ( 12 | __version__ 13 | == toml.loads(project_dir.joinpath("pyproject.toml").read_text())["tool"][ 14 | "poetry" 15 | ]["version"] 16 | ) 17 | -------------------------------------------------------------------------------- /unmarkd/__init__.py: -------------------------------------------------------------------------------- 1 | """A markdown reverser.""" 2 | 3 | from typing import TYPE_CHECKING, Union 4 | 5 | if TYPE_CHECKING: 6 | import bs4 7 | 8 | from . import unmarkers 9 | 10 | 11 | def unmark(html: Union[str, "bs4.NavigableString", "bs4.BeautifulSoup"]) -> str: 12 | """Convert HTML to markdown.""" 13 | return unmarkers.BasicUnmarker().unmark(html) 14 | 15 | 16 | __version__ = "1.1.3" 17 | -------------------------------------------------------------------------------- /unmarkd/unmarkers.py: -------------------------------------------------------------------------------- 1 | """Generate markdown from messy HTML.""" 2 | 3 | import abc 4 | import contextlib 5 | import html as lib_html 6 | import textwrap 7 | from typing import Callable, Dict, Set, Union 8 | 9 | import bs4 10 | 11 | SoupElement = Union[bs4.PageElement, bs4.BeautifulSoup] 12 | 13 | 14 | class BaseUnmarker(abc.ABC): 15 | """To customize the behavior of your reverser, inherit from this abstract class.""" 16 | 17 | ESCAPABLES: Set[str] = { 18 | "*", 19 | "`", 20 | "\\", 21 | "~", 22 | "_", 23 | "-", 24 | "[", 25 | "]", 26 | ">", 27 | "#", 28 | } 29 | TAG_ALIASES: Dict[str, str] = {} 30 | DEFAULT_TAG_ALIASES: Dict[str, str] = {"em": "i", "strong": "b", "s": "del"} 31 | UNORDERED_FORMAT: str = "\n- {next_item}" 32 | ORDERED_FORMAT: str = "\n{number_index}. {next_item}" 33 | 34 | # def parse_css(self: "BaseUnmarker", css: str) -> Dict[str, str]: 35 | 36 | def _render_list( 37 | self: "BaseUnmarker", 38 | element: bs4.Tag, 39 | item_format: str, 40 | counter_initial_value: int = 1, 41 | ) -> str: 42 | """Render a list of items according to a format. 43 | 44 | Made to reduce code duplication. 45 | """ 46 | output = "" 47 | counter = counter_initial_value 48 | for child in element.children: 49 | if non_tag_output := self.parse_non_tags(child): 50 | if non_tag_output.strip() == "": 51 | continue 52 | output += non_tag_output 53 | continue 54 | assert isinstance(child, bs4.Tag), type(element) 55 | output += item_format.format( 56 | next_item=self.tag_li(child).rstrip(), 57 | number_index=counter, 58 | ) 59 | counter += 1 60 | return output.lstrip("\n") 61 | 62 | def escape(self: "BaseUnmarker", string: str) -> str: 63 | """Escape a string to be markdown-safe.""" 64 | return "".join(map(self.__escape_character, string)) 65 | 66 | def __escape_character(self: "BaseUnmarker", char: str) -> str: 67 | """Escape a single character.""" 68 | assert len(char) == 1 69 | if char in self.ESCAPABLES: 70 | return "\\" + char 71 | return lib_html.escape(char) 72 | 73 | def wrap(self: "BaseUnmarker", element: SoupElement, around_with: str) -> str: 74 | """Wrap an element in a markdown-safe manner. 75 | 76 | Parameters 77 | ---------- 78 | element : bs4.BeautifulSoup 79 | The element to wrap. 80 | around_with : str 81 | What to wrap `element` around with. 82 | 83 | Notes 84 | ----- 85 | `around_with` will not be escaped. 86 | 87 | Returns 88 | ------- 89 | str 90 | The wrapped version of the element. 91 | 92 | """ 93 | return around_with + self.__parse(element, escape=True) + around_with 94 | 95 | def handle_tag(self: "BaseUnmarker", tag: bs4.Tag) -> str: 96 | return self.resolve_handler_func(tag.name)(tag) 97 | 98 | def resolve_handler_func( 99 | self: "BaseUnmarker", 100 | name: str, 101 | ) -> Callable[[bs4.Tag], str]: 102 | return getattr(self, "tag_" + name) # type: ignore[no-any-return] 103 | 104 | def handle_string(self: "BaseUnmarker", string: str) -> str: 105 | if string == "\n": 106 | return "\n\n" 107 | return self.escape(string) if self._should_escape else string 108 | 109 | def handle_doctype(self: "BaseUnmarker", _: bs4.Doctype) -> str: 110 | return "" 111 | 112 | def handle_cdata(self: "BaseUnmarker", _: bs4.CData) -> str: 113 | return "" 114 | 115 | def handle_declaration(self: "BaseUnmarker", _: bs4.Declaration) -> str: 116 | return "" 117 | 118 | def handle_processing_instruction( 119 | self: "BaseUnmarker", 120 | _: bs4.ProcessingInstruction, 121 | ) -> str: 122 | return "" 123 | 124 | def handle_comment(self: "BaseUnmarker", child: bs4.Comment) -> str: 125 | """Self explanatory.""" 126 | # Should not cause any escaping problems since 127 | # BeautifulSoup escapes the string contents 128 | # See also https://www.crummy.com/software/BeautifulSoup/bs4/doc/#output-formatters 129 | return f"" 130 | 131 | def parse_non_tags(self: "BaseUnmarker", child: bs4.PageElement) -> str: 132 | # Function generated via ChatGPT 133 | def pascal_to_snake(pascal_string: str) -> str: 134 | snake_string = "" 135 | for i, char in enumerate(pascal_string): 136 | if i > 0 and char.isupper(): 137 | snake_string += "_" 138 | snake_string += char.lower() 139 | return snake_string 140 | 141 | if ( 142 | issubclass(type(child), bs4.NavigableString) 143 | and type(child) is not bs4.NavigableString 144 | ): 145 | try: 146 | return getattr( # type: ignore[no-any-return] 147 | self, 148 | f"handle_{pascal_to_snake(type(child).__name__)}", 149 | )( 150 | child, 151 | ) 152 | except AttributeError as error: 153 | msg = "This should never happen" 154 | raise AssertionError(msg, type(child)) from error 155 | if isinstance(child, (str, bs4.NavigableString)): 156 | return self.handle_string(child) 157 | return "" # To indicate that it is a tag 158 | 159 | def __parse( 160 | self: "BaseUnmarker", 161 | html: SoupElement, 162 | escape: bool = False, 163 | ) -> str: 164 | """Parse an HTML element into valid markdown.""" 165 | self._should_escape = escape # for handle_string 166 | output = "" 167 | if html is None: 168 | return "" 169 | for child in html.children: # type: ignore[union-attr] 170 | if isinstance(child, bs4.Tag): 171 | name: str = child.name 172 | # To reduce code duplication 173 | name = ( 174 | self.DEFAULT_TAG_ALIASES.get(name) 175 | or self.TAG_ALIASES.get(name) 176 | or name 177 | ) 178 | 179 | try: 180 | output += self.resolve_handler_func(name)(child) 181 | except AttributeError: 182 | output += self.handle_default(child) 183 | else: 184 | output += self.parse_non_tags(child) 185 | return output 186 | 187 | def handle_default(self: "BaseUnmarker", child: bs4.PageElement) -> str: 188 | """Whenever a tag isn't handled by one of these methods, this is called.""" 189 | return str(child) 190 | 191 | # fmt: off 192 | def tag_h1(self: "BaseUnmarker", child: bs4.Tag) -> str: 193 | return "# " + self.__parse(child) + "\n" 194 | def tag_h2(self: "BaseUnmarker", child: bs4.Tag) -> str: 195 | return "## " + self.__parse(child) + "\n" 196 | def tag_h3(self: "BaseUnmarker", child: bs4.Tag) -> str: 197 | return "### " + self.__parse(child) + "\n" 198 | def tag_h4(self: "BaseUnmarker", child: bs4.Tag) -> str: 199 | return "#### " + self.__parse(child) + "\n" 200 | def tag_h5(self: "BaseUnmarker", child: bs4.Tag) -> str: 201 | return "##### " + self.__parse(child) + "\n" 202 | def tag_h6(self: "BaseUnmarker", child: bs4.Tag) -> str: 203 | return "###### " + self.__parse(child) + "\n" 204 | # fmt: on 205 | 206 | def tag_div(self: "BaseUnmarker", child: bs4.Tag) -> str: 207 | return self.__parse(child) 208 | 209 | def tag_p(self: "BaseUnmarker", child: bs4.Tag) -> str: 210 | return self.__parse(child, escape=True) 211 | 212 | def tag_del(self: "BaseUnmarker", child: bs4.Tag) -> str: 213 | return self.wrap(child, around_with="~~") 214 | 215 | def tag_pre(self: "BaseUnmarker", child: bs4.Tag) -> str: 216 | assert child.code is not None 217 | return f"\n```{self.detect_language(child)}\n{child.code.get_text()}```\n" 218 | 219 | def tag_code(self: "BaseUnmarker", child: bs4.Tag) -> str: 220 | return f"`{self.__parse(child)}`" 221 | 222 | def tag_hr(self: "BaseUnmarker", _: bs4.BeautifulSoup) -> str: 223 | return "\n---\n" 224 | 225 | def tag_td(self: "BaseUnmarker", child: bs4.Tag) -> str: 226 | return self.__parse(child, escape=True) 227 | 228 | def tag_b(self: "BaseUnmarker", child: bs4.Tag) -> str: 229 | return self.wrap(child, around_with="**") 230 | 231 | def tag_i(self: "BaseUnmarker", child: bs4.Tag) -> str: 232 | return self.wrap(child, around_with="*") 233 | 234 | def tag_a(self: "BaseUnmarker", child: bs4.Tag) -> str: 235 | return ( 236 | f"[{self.__parse(child)}]({child['href']}" 237 | + ( 238 | " " + repr(self.escape(child["title"])) # type: ignore[arg-type] 239 | if child.get("title") 240 | else "" 241 | ) 242 | + ")" 243 | ) 244 | 245 | def tag_img(self: "BaseUnmarker", img: bs4.Tag) -> str: 246 | img_text = "" 247 | if (parent := img.parent) is not None and ( 248 | figcaption := parent.find("figcaption") 249 | ) is not None: 250 | img_text = figcaption.get_text() 251 | return f"![{img.get('alt') or img_text}]({img['src']})" 252 | 253 | def tag_ul(self: "BaseUnmarker", child: bs4.Tag) -> str: 254 | return self._render_list(child, self.UNORDERED_FORMAT) 255 | 256 | def tag_ol(self: "BaseUnmarker", child: bs4.Tag) -> str: 257 | return self._render_list( 258 | child, 259 | self.ORDERED_FORMAT, 260 | counter_initial_value=int(child.get("start", 1)), # type: ignore[arg-type] 261 | ) 262 | 263 | def tag_li(self: "BaseUnmarker", child: bs4.Tag) -> str: 264 | output = "" 265 | for element in child.children: 266 | if (non_tag_output := self.parse_non_tags(element)).strip() != "": 267 | output += non_tag_output 268 | continue 269 | assert isinstance(element, bs4.Tag), type(element) 270 | if element.name in ("ol", "ul"): 271 | output += textwrap.indent( 272 | self.handle_tag(element), 273 | " ", 274 | ) 275 | else: 276 | output += self.handle_tag(element) 277 | return output 278 | 279 | def tag_br(self: "BaseUnmarker", _: bs4.BeautifulSoup) -> str: 280 | return "\n\n" 281 | 282 | def tag_blockquote(self: "BaseUnmarker", child: bs4.Tag) -> str: 283 | return ">" + self.__parse(child).strip() + "\n" 284 | 285 | def tag_q(self: "BaseUnmarker", child: bs4.Tag) -> str: 286 | return self.wrap(child, around_with='"') 287 | 288 | def unmark(self: "BaseUnmarker", html: Union[str, bs4.BeautifulSoup]) -> str: 289 | """Convert HTML into markdown.""" 290 | if type(html) != bs4.BeautifulSoup: 291 | assert isinstance(html, str) 292 | html = bs4.BeautifulSoup(html, "html.parser") 293 | assert isinstance(html, bs4.BeautifulSoup) 294 | if html.html is not None: # Testing if not using "html.parser" 295 | html.html.unwrap() # Maintaining lxml and html5lib compatibility 296 | if html.head is not None: # html5lib compatibility... for the future 297 | html.head.decompose() 298 | if html.body is not None: # lxml compatibility 299 | html.body.unwrap() 300 | return self.__parse(html).strip().replace("\u0000", "\uFFFD") 301 | 302 | def detect_language( 303 | self: "BaseUnmarker", 304 | html: bs4.Tag, 305 | ) -> str: # XXX: Replace with info string 306 | """From a block of HTML, detect the language from the class attribute. 307 | 308 | Warning 309 | ------- 310 | The default is very dumb and will return the first class. 311 | 312 | Parameters 313 | ---------- 314 | html : bs4.BeautifulSoup 315 | The block of HTML to parse from `html`. 316 | 317 | Returns 318 | ------- 319 | str 320 | The found language. If no language if found, return "". 321 | 322 | """ 323 | classes = html.get("class") or html.code.get("class") if html.code else None 324 | if not classes: 325 | return "" 326 | return classes[0] or "" 327 | 328 | 329 | class StackOverflowUnmarker(BaseUnmarker): 330 | """A specialized unmarker for HTML found on StackOverflow.""" 331 | 332 | def detect_language(self: "BaseUnmarker", html: bs4.Tag) -> str: 333 | assert html.code is not None 334 | classes = html.get("class") or html.code.get("class") 335 | if classes is None: 336 | return "" 337 | assert isinstance(classes, list) 338 | for item in ( 339 | "default", 340 | "s-code-block", 341 | "hljs", 342 | "snippet-code-js", 343 | "prettyprint-override", 344 | ): 345 | with contextlib.suppress(ValueError): 346 | classes.remove(item) 347 | 348 | if len(classes) == 0: 349 | return "" 350 | output = classes[0] 351 | if output.startswith("lang-"): 352 | return output[5:] 353 | if output.startswith("snippet-code-"): 354 | return output[13:] 355 | return output 356 | 357 | 358 | StackExchangeUnmarker = StackOverflowUnmarker 359 | 360 | 361 | class BasicUnmarker(BaseUnmarker): 362 | """The basic, generic unmarker.""" 363 | 364 | def detect_language(self: "BaseUnmarker", html: bs4.Tag) -> str: 365 | classes = html.get("class") or (html.code.get("class") if html.code else None) 366 | if classes is None: 367 | return "" 368 | classes = classes[:] # Copy it 369 | assert len(classes) == 1 370 | lang = classes[0] 371 | if lang.startswith("lang-"): 372 | return lang[5:] 373 | if lang.startswith("language-"): 374 | return lang[9:] 375 | return lang 376 | --------------------------------------------------------------------------------