├── .appveyor.yml ├── .coveragerc ├── .flake8 ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── apertium ├── __init__.py ├── analysis │ └── __init__.py ├── generation │ └── __init__.py ├── installer.py ├── iso639.py ├── mode_search.py ├── py.typed ├── tagger │ └── __init__.py ├── translation │ └── __init__.py └── utils.py ├── docs ├── Makefile └── source │ ├── about.md │ ├── conf.py │ ├── index.md │ ├── install.md │ └── usage.md ├── setup.py └── tests └── __init__.py /.appveyor.yml: -------------------------------------------------------------------------------- 1 | clone_depth: 50 2 | cache: 3 | - '%LOCALAPPDATA%\pip\Cache' 4 | environment: 5 | matrix: 6 | - PYTHON: C:\\Python36 7 | PYTHON_VERSION: 3.6 8 | PYTHON_ARCH: 32 9 | - PYTHON: C:\\Python36-x64 10 | PYTHON_VERSION: 3.6 11 | PYTHON_ARCH: 64 12 | - PYTHON: C:\\Python37 13 | PYTHON_VERSION: 3.7 14 | PYTHON_ARCH: 32 15 | - PYTHON: C:\\Python37-x64 16 | PYTHON_VERSION: 3.7 17 | PYTHON_ARCH: 64 18 | - PYTHON: C:\\Python38 19 | PYTHON_VERSION: 3.8 20 | PYTHON_ARCH: 32 21 | - PYTHON: C:\\Python38-x64 22 | PYTHON_VERSION: 3.8 23 | PYTHON_ARCH: 64 24 | init: 25 | - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" 26 | - "ECHO \"%APPVEYOR_SCHEDULED_BUILD%\"" 27 | - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` 28 | https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` 29 | Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` 30 | raise "There are newer queued builds for this pull request, skipping build." 31 | } # credits: JuliaLang developers 32 | install: 33 | - SET PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% 34 | - python --version 35 | - python -c "import sys,platform,struct; print(sys.platform, platform.machine(), struct.calcsize('P') * 8, )" 36 | - pip install pipenv 37 | - pipenv install --dev --system 38 | - python setup.py install 39 | build: false 40 | test_script: 41 | - coverage run -m unittest --verbose --buffer tests 42 | - coverage report --show-missing --fail-under 70 --include 'apertium/**' 43 | artifacts: 44 | - path: dist\* 45 | notifications: 46 | - provider: Email 47 | on_build_success: false 48 | on_build_failure: true 49 | on_build_status_changed: true 50 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit=apertium/installer.py 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = W504 3 | max-line-length = 160 4 | import-order-style = google 5 | exclude = docs/source/conf.py 6 | application-import-names = apertium 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .mypy_coverage/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: bionic 3 | cache: 4 | pip: true 5 | directories: 6 | - /usr/local/bin 7 | - /usr/local/lib/python$TRAVIS_PYTHON_VERSION/site-packages 8 | python: 9 | - '3.6' 10 | - '3.7' 11 | - '3.8' 12 | - 'nightly' 13 | install: 14 | - pip install pipenv 15 | - python3 setup.py install 16 | - pipenv install --dev --system 17 | script: 18 | - flake8 --verbose apertium 19 | - mypy apertium --strict --any-exprs-report .mypy_coverage --ignore-missing-imports; 20 | cat .mypy_coverage/any-exprs.txt; 21 | coverage=$(tail -1 .mypy_coverage/any-exprs.txt | grep -Eo '[0-9\.]+%' | sed 's/%$//'); 22 | if (( $(echo "$coverage < 95" | bc -l) )); then 23 | exit 1; 24 | fi; 25 | - coverage run -m unittest --verbose --buffer tests 26 | - coverage report --show-missing --fail-under 90 --include 'apertium/**' 27 | after_success: 28 | - coveralls 29 | notifications: 30 | on_failure: change 31 | on_success: change 32 | matrix: 33 | allow_failures: 34 | - python: nightly 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | dist: 4 | git clean -xfd 5 | ./setup.py sdist 6 | 7 | release: dist 8 | twine upload --sign dist/* 9 | 10 | test-release: dist 11 | twine upload --sign --repository-url https://test.pypi.org/legacy/ dist/* 12 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | coverage = "*" 8 | coveralls = "*" 9 | flake8 = "*" 10 | flake8-bugbear = "*" 11 | flake8-builtins = "*" 12 | flake8-commas = "*" 13 | flake8-comprehensions = "*" 14 | flake8-eradicate = {version = "*",markers = "python_version > '3.5'"} 15 | flake8-import-order = "*" 16 | flake8-quotes = "*" 17 | mypy = "*" 18 | pep8-naming = "*" 19 | sphinx = "*" 20 | twine = "*" 21 | recommonmark = "*" 22 | 23 | [packages] 24 | apertium-streamparser = "*" 25 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b6374fdee852c76c04687a0a448a27e74a2af1a82468b3c7ec60086d6cd7e81d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "apertium-streamparser": { 18 | "hashes": [ 19 | "sha256:e14e99f9a682725b6f8c0955f86d79319d7786d2e43b1dcaa50f4151b0410771", 20 | "sha256:fd83d3d573d23c54b34339865cdd40cded3687311c18629d2d39c4e8ad1da597" 21 | ], 22 | "index": "pypi", 23 | "version": "==5.0.2" 24 | } 25 | }, 26 | "develop": { 27 | "alabaster": { 28 | "hashes": [ 29 | "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", 30 | "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" 31 | ], 32 | "version": "==0.7.12" 33 | }, 34 | "attrs": { 35 | "hashes": [ 36 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 37 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 38 | ], 39 | "version": "==19.3.0" 40 | }, 41 | "babel": { 42 | "hashes": [ 43 | "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", 44 | "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" 45 | ], 46 | "version": "==2.9.0" 47 | }, 48 | "bleach": { 49 | "hashes": [ 50 | "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", 51 | "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" 52 | ], 53 | "version": "==3.3.0" 54 | }, 55 | "certifi": { 56 | "hashes": [ 57 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 58 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 59 | ], 60 | "version": "==2020.12.5" 61 | }, 62 | "chardet": { 63 | "hashes": [ 64 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 65 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 66 | ], 67 | "version": "==4.0.0" 68 | }, 69 | "commonmark": { 70 | "hashes": [ 71 | "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", 72 | "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" 73 | ], 74 | "version": "==0.9.1" 75 | }, 76 | "coverage": { 77 | "hashes": [ 78 | "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", 79 | "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", 80 | "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", 81 | "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", 82 | "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", 83 | "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", 84 | "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", 85 | "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", 86 | "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", 87 | "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", 88 | "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", 89 | "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", 90 | "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", 91 | "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", 92 | "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", 93 | "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", 94 | "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", 95 | "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", 96 | "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", 97 | "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", 98 | "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", 99 | "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", 100 | "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", 101 | "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", 102 | "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", 103 | "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", 104 | "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", 105 | "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", 106 | "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", 107 | "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", 108 | "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", 109 | "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" 110 | ], 111 | "index": "pypi", 112 | "version": "==4.5.4" 113 | }, 114 | "coveralls": { 115 | "hashes": [ 116 | "sha256:9bc5a1f92682eef59f688a8f280207190d9a6afb84cef8f567fa47631a784060", 117 | "sha256:fb51cddef4bc458de347274116df15d641a735d3f0a580a9472174e2e62f408c" 118 | ], 119 | "index": "pypi", 120 | "version": "==1.8.2" 121 | }, 122 | "docopt": { 123 | "hashes": [ 124 | "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" 125 | ], 126 | "version": "==0.6.2" 127 | }, 128 | "docutils": { 129 | "hashes": [ 130 | "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", 131 | "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" 132 | ], 133 | "version": "==0.16" 134 | }, 135 | "entrypoints": { 136 | "hashes": [ 137 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 138 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 139 | ], 140 | "version": "==0.3" 141 | }, 142 | "eradicate": { 143 | "hashes": [ 144 | "sha256:4ffda82aae6fd49dfffa777a857cb758d77502a1f2e0f54c9ac5155a39d2d01a" 145 | ], 146 | "version": "==1.0" 147 | }, 148 | "flake8": { 149 | "hashes": [ 150 | "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", 151 | "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" 152 | ], 153 | "index": "pypi", 154 | "version": "==3.7.8" 155 | }, 156 | "flake8-bugbear": { 157 | "hashes": [ 158 | "sha256:d8c466ea79d5020cb20bf9f11cf349026e09517a42264f313d3f6fddb83e0571", 159 | "sha256:ded4d282778969b5ab5530ceba7aa1a9f1b86fa7618fc96a19a1d512331640f8" 160 | ], 161 | "index": "pypi", 162 | "version": "==19.8.0" 163 | }, 164 | "flake8-builtins": { 165 | "hashes": [ 166 | "sha256:8d806360767947c0035feada4ddef3ede32f0a586ef457e62d811b8456ad9a51", 167 | "sha256:cd7b1b7fec4905386a3643b59f9ca8e305768da14a49a7efb31fe9364f33cd04" 168 | ], 169 | "index": "pypi", 170 | "version": "==1.4.1" 171 | }, 172 | "flake8-commas": { 173 | "hashes": [ 174 | "sha256:d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44b7", 175 | "sha256:ee2141a3495ef9789a3894ed8802d03eff1eaaf98ce6d8653a7c573ef101935e" 176 | ], 177 | "index": "pypi", 178 | "version": "==2.0.0" 179 | }, 180 | "flake8-comprehensions": { 181 | "hashes": [ 182 | "sha256:7b174ded3d7e73edf587e942458b6c1a7c3456d512d9c435deae367236b9562c", 183 | "sha256:e36fc12bd3833e0b34fe0639b7a817d32c86238987f532078c57eafdc7a8a219" 184 | ], 185 | "index": "pypi", 186 | "version": "==2.2.0" 187 | }, 188 | "flake8-eradicate": { 189 | "hashes": [ 190 | "sha256:86804c682f9805a689379307939f350140fe9c015c3e600baff37fb23f7f21cc", 191 | "sha256:cc2c3300a6643f8347988cc828478c347975f7bf9c8fc1f8a7027da41ab9bdbd" 192 | ], 193 | "index": "pypi", 194 | "markers": "python_version > '3.5'", 195 | "version": "==0.2.1" 196 | }, 197 | "flake8-import-order": { 198 | "hashes": [ 199 | "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543", 200 | "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92" 201 | ], 202 | "index": "pypi", 203 | "version": "==0.18.1" 204 | }, 205 | "flake8-polyfill": { 206 | "hashes": [ 207 | "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", 208 | "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda" 209 | ], 210 | "version": "==1.0.2" 211 | }, 212 | "flake8-quotes": { 213 | "hashes": [ 214 | "sha256:5dbaf668887873f28346fb87943d6da2e4b9f77ce9f2169cff21764a0a4934ed" 215 | ], 216 | "index": "pypi", 217 | "version": "==2.1.0" 218 | }, 219 | "idna": { 220 | "hashes": [ 221 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 222 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 223 | ], 224 | "version": "==2.10" 225 | }, 226 | "imagesize": { 227 | "hashes": [ 228 | "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", 229 | "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" 230 | ], 231 | "version": "==1.2.0" 232 | }, 233 | "jinja2": { 234 | "hashes": [ 235 | "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", 236 | "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" 237 | ], 238 | "index": "pypi", 239 | "version": "==2.11.3" 240 | }, 241 | "markupsafe": { 242 | "hashes": [ 243 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 244 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 245 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 246 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 247 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 248 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 249 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 250 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 251 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 252 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 253 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 254 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 255 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 256 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 257 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 258 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 259 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 260 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 261 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 262 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 263 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 264 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 265 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 266 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 267 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 268 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 269 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 270 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 271 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 272 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 273 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 274 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 275 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 276 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 277 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 278 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 279 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 280 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 281 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 282 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 283 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 284 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 285 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 286 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 287 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 288 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 289 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 290 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 291 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 292 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 293 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 294 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 295 | ], 296 | "version": "==1.1.1" 297 | }, 298 | "mccabe": { 299 | "hashes": [ 300 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 301 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 302 | ], 303 | "version": "==0.6.1" 304 | }, 305 | "mypy": { 306 | "hashes": [ 307 | "sha256:0107bff4f46a289f0e4081d59b77cef1c48ea43da5a0dbf0005d54748b26df2a", 308 | "sha256:07957f5471b3bb768c61f08690c96d8a09be0912185a27a68700f3ede99184e4", 309 | "sha256:10af62f87b6921eac50271e667cc234162a194e742d8e02fc4ddc121e129a5b0", 310 | "sha256:11fd60d2f69f0cefbe53ce551acf5b1cec1a89e7ce2d47b4e95a84eefb2899ae", 311 | "sha256:15e43d3b1546813669bd1a6ec7e6a11d2888db938e0607f7b5eef6b976671339", 312 | "sha256:352c24ba054a89bb9a35dd064ee95ab9b12903b56c72a8d3863d882e2632dc76", 313 | "sha256:437020a39417e85e22ea8edcb709612903a9924209e10b3ec6d8c9f05b79f498", 314 | "sha256:49925f9da7cee47eebf3420d7c0e00ec662ec6abb2780eb0a16260a7ba25f9c4", 315 | "sha256:6724fcd5777aa6cebfa7e644c526888c9d639bd22edd26b2a8038c674a7c34bd", 316 | "sha256:7a17613f7ea374ab64f39f03257f22b5755335b73251d0d253687a69029701ba", 317 | "sha256:cdc1151ced496ca1496272da7fc356580e95f2682be1d32377c22ddebdf73c91" 318 | ], 319 | "index": "pypi", 320 | "version": "==0.720" 321 | }, 322 | "mypy-extensions": { 323 | "hashes": [ 324 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 325 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 326 | ], 327 | "version": "==0.4.3" 328 | }, 329 | "packaging": { 330 | "hashes": [ 331 | "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", 332 | "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" 333 | ], 334 | "version": "==20.9" 335 | }, 336 | "pep8-naming": { 337 | "hashes": [ 338 | "sha256:01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9", 339 | "sha256:0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb" 340 | ], 341 | "index": "pypi", 342 | "version": "==0.8.2" 343 | }, 344 | "pkginfo": { 345 | "hashes": [ 346 | "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", 347 | "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" 348 | ], 349 | "version": "==1.7.0" 350 | }, 351 | "pycodestyle": { 352 | "hashes": [ 353 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 354 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 355 | ], 356 | "version": "==2.5.0" 357 | }, 358 | "pyflakes": { 359 | "hashes": [ 360 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 361 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 362 | ], 363 | "version": "==2.1.1" 364 | }, 365 | "pygments": { 366 | "hashes": [ 367 | "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", 368 | "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" 369 | ], 370 | "version": "==2.8.1" 371 | }, 372 | "pyparsing": { 373 | "hashes": [ 374 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 375 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 376 | ], 377 | "version": "==2.4.7" 378 | }, 379 | "pytz": { 380 | "hashes": [ 381 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", 382 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" 383 | ], 384 | "version": "==2021.1" 385 | }, 386 | "readme-renderer": { 387 | "hashes": [ 388 | "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", 389 | "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" 390 | ], 391 | "version": "==29.0" 392 | }, 393 | "recommonmark": { 394 | "hashes": [ 395 | "sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb", 396 | "sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852" 397 | ], 398 | "index": "pypi", 399 | "version": "==0.6.0" 400 | }, 401 | "requests": { 402 | "hashes": [ 403 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 404 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 405 | ], 406 | "version": "==2.25.1" 407 | }, 408 | "requests-toolbelt": { 409 | "hashes": [ 410 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 411 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 412 | ], 413 | "version": "==0.9.1" 414 | }, 415 | "six": { 416 | "hashes": [ 417 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 418 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 419 | ], 420 | "version": "==1.15.0" 421 | }, 422 | "snowballstemmer": { 423 | "hashes": [ 424 | "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", 425 | "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" 426 | ], 427 | "version": "==2.1.0" 428 | }, 429 | "sphinx": { 430 | "hashes": [ 431 | "sha256:0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845", 432 | "sha256:839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069" 433 | ], 434 | "index": "pypi", 435 | "version": "==2.2.0" 436 | }, 437 | "sphinxcontrib-applehelp": { 438 | "hashes": [ 439 | "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", 440 | "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" 441 | ], 442 | "version": "==1.0.2" 443 | }, 444 | "sphinxcontrib-devhelp": { 445 | "hashes": [ 446 | "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", 447 | "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" 448 | ], 449 | "version": "==1.0.2" 450 | }, 451 | "sphinxcontrib-htmlhelp": { 452 | "hashes": [ 453 | "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", 454 | "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" 455 | ], 456 | "version": "==1.0.3" 457 | }, 458 | "sphinxcontrib-jsmath": { 459 | "hashes": [ 460 | "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", 461 | "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" 462 | ], 463 | "version": "==1.0.1" 464 | }, 465 | "sphinxcontrib-qthelp": { 466 | "hashes": [ 467 | "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", 468 | "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" 469 | ], 470 | "version": "==1.0.3" 471 | }, 472 | "sphinxcontrib-serializinghtml": { 473 | "hashes": [ 474 | "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", 475 | "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" 476 | ], 477 | "version": "==1.1.4" 478 | }, 479 | "tqdm": { 480 | "hashes": [ 481 | "sha256:9fdf349068d047d4cfbe24862c425883af1db29bcddf4b0eeb2524f6fbdb23c7", 482 | "sha256:d666ae29164da3e517fcf125e41d4fe96e5bb375cd87ff9763f6b38b5592fe33" 483 | ], 484 | "version": "==4.59.0" 485 | }, 486 | "twine": { 487 | "hashes": [ 488 | "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446", 489 | "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc" 490 | ], 491 | "index": "pypi", 492 | "version": "==1.13.0" 493 | }, 494 | "typed-ast": { 495 | "hashes": [ 496 | "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", 497 | "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", 498 | "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", 499 | "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", 500 | "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", 501 | "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", 502 | "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", 503 | "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", 504 | "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", 505 | "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", 506 | "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", 507 | "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", 508 | "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", 509 | "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", 510 | "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", 511 | "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", 512 | "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", 513 | "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", 514 | "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", 515 | "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", 516 | "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", 517 | "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", 518 | "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", 519 | "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", 520 | "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", 521 | "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", 522 | "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", 523 | "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", 524 | "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", 525 | "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" 526 | ], 527 | "version": "==1.4.2" 528 | }, 529 | "typing-extensions": { 530 | "hashes": [ 531 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 532 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 533 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 534 | ], 535 | "version": "==3.7.4.3" 536 | }, 537 | "urllib3": { 538 | "hashes": [ 539 | "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", 540 | "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" 541 | ], 542 | "version": "==1.26.4" 543 | }, 544 | "webencodings": { 545 | "hashes": [ 546 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 547 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 548 | ], 549 | "version": "==0.5.1" 550 | } 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apertium + Python 2 | 3 | [![Travis Build Status](https://travis-ci.com/apertium/apertium-python.svg?branch=master)](https://travis-ci.com/apertium/apertium-python) 4 | [![Appveyor Build status](https://ci.appveyor.com/api/projects/status/sesdinoy4cw2p1tk/branch/master?svg=true)](https://ci.appveyor.com/project/apertium/apertium-python/branch/master) 5 | [![ReadTheDocs Docs Status](https://readthedocs.org/projects/apertium-python/badge)](https://readthedocs.org/projects/apertium-python) 6 | [![Coverage Status](https://coveralls.io/repos/github/apertium/apertium-python/badge.svg?branch=master)](https://coveralls.io/github/apertium/apertium-python?branch=master) 7 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/apertium.svg)]((https://pypi.org/project/apertium/)) 8 | 9 | ## Introduction 10 | 11 | - The codebase is in development for the GSoC '19 project called **Apertium API in Python** 12 | - The Apertium core modules are written in C++. 13 | - This project makes the Apertium modules available in Python, which because of its simplicity is more appealing to users. 14 | 15 | ## About the Exisiting Code Base 16 | 17 | - The existing codebase has `Subprocess` and [SWIG](http://www.swig.org/) wrapper implementations of the higher level functions of Apertium and CG modules. 18 | 19 | ## Installation 20 | 21 | - Installation on Debian/Ubuntu and Windows is natively supported: 22 | 23 | ``` 24 | pip install apertium 25 | ``` 26 | 27 | - For developers, `pipenv` can be used to install the development dependencies and enter a shell with them: 28 | 29 | ``` 30 | pip install pipenv 31 | pipenv install --dev 32 | pipenv shell 33 | ``` 34 | 35 | - Apertium packages can be installed from Python interpreter as well. 36 | - Install `apertium-all-dev` by calling `apertium.installer.install_apertium()` 37 | - Install language packages with `apertium.installer.install_module(language_name)`. For example `apertium-eng` can be installed by executing `apertium.installer.install_module('eng')` 38 | 39 | ## Usage 40 | 41 | - For multiple invocations `Method 1` is more performant, as the dictionary needs to be loaded only once. 42 | 43 | ### Analysis 44 | 45 | Performing Morphological Analysis 46 | 47 | Method 1: Create an `Analyzer` object and call its `analyze` method. 48 | ```python 49 | In [1]: import apertium 50 | In [2]: a = apertium.Analyzer('en') 51 | In [3]: a.analyze('cats') 52 | Out[3]: [cats/cat, ./.] 53 | ``` 54 | 55 | Method 2: Calling `analyze()` directly. 56 | ```python 57 | In [1]: import apertium 58 | In [2]: apertium.analyze('en', 'cats') 59 | Out[2]: cats/cat 60 | ``` 61 | 62 | ### Generation 63 | 64 | Performing Morphological Generation 65 | 66 | Method 1: Create a `Generator` object and call its `generate` method. 67 | ```python 68 | In [1]: import apertium 69 | In [2]: g = apertium.Generator('en') 70 | In [3]: g.generate('^cat$') 71 | Out[3]: 'cats' 72 | ``` 73 | 74 | Method 2: Calling `generate()` directly. 75 | ```python 76 | In [1]: import apertium 77 | In [2]: apertium.generate('en', '^cat$') 78 | Out[2]: 'cats' 79 | ``` 80 | 81 | ### Tagger 82 | 83 | Method 1: Create a `Tagger` object and call its `tag` method. 84 | ```python 85 | In [1]: import apertium 86 | In [2]: tagger = apertium.Tagger('eng') 87 | In [3]: tagger.tag('cats') 88 | Out[3]: [cats/cat] 89 | ``` 90 | 91 | Method 2: Calling `tag()` directly. 92 | ```python 93 | In [1]: import apertium 94 | In [2]: apertium.tag('en', 'cats') 95 | Out[2]: [cats/cat] 96 | ``` 97 | 98 | ### Translation 99 | 100 | Method 1: Create a `Translator` object and call its `translate` method. 101 | ```python 102 | In [1]: import apertium 103 | In [2]: t = apertium.Translator('eng', 'spa') 104 | In [3]: t.translate('cats') 105 | Out[3]: 'Gatos' 106 | ``` 107 | 108 | Method 2: Calling `translate()` directly. 109 | ```python 110 | In [1]: import apertium 111 | In [2]: apertium.translate('en', 'spa', 'cats') 112 | Out[2]: 'Gatos' 113 | ``` 114 | 115 | ### Installing more modes from other language data 116 | 117 | One can also install modes by providing the path to the `lang-data`: 118 | 119 | ```python 120 | In [1]: import apertium 121 | In [2]: apertium.append_pair_path('..') 122 | ``` 123 | -------------------------------------------------------------------------------- /apertium/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Lokendra Singh, Arghya Bhatttacharya, Sushain K. Cherivirala, Andi Qu' 2 | __license__ = 'GNU General Public License v3.0' 3 | __version__ = '0.2.4' 4 | 5 | import logging 6 | import os 7 | import platform 8 | from typing import Dict, Tuple 9 | 10 | from apertium.analysis import analyze, Analyzer # noqa: F401 11 | from apertium.generation import generate, Generator # noqa: F401 12 | from apertium.installer import install_module # noqa: F401 13 | from apertium.mode_search import search_path 14 | from apertium.tagger import tag, Tagger # noqa: F401 15 | from apertium.translation import translate, Translator # noqa: F401 16 | from apertium.utils import wrappers_available # noqa: F401 17 | 18 | 19 | class ModeNotInstalled(ValueError): 20 | pass 21 | 22 | 23 | class InstallationNotSupported(ValueError): 24 | pass 25 | 26 | 27 | def _update_modes() -> None: 28 | """ 29 | 30 | """ 31 | for pair_path in pair_paths: 32 | modes = search_path(pair_path) 33 | if modes['pair']: 34 | for path, lang_src, lang_trg in modes['pair']: 35 | pairs[f'{lang_src}-{lang_trg}'] = path 36 | if modes['analyzer']: 37 | for dirpath, modename, lang_pair in modes['analyzer']: 38 | analyzers[lang_pair] = (dirpath, modename) 39 | if modes['generator']: 40 | for dirpath, modename, lang_pair in modes['generator']: 41 | generators[lang_pair] = (dirpath, modename) 42 | if modes['tagger']: 43 | for dirpath, modename, lang_pair in modes['tagger']: 44 | taggers[lang_pair] = (dirpath, modename) 45 | 46 | 47 | def append_pair_path(pair_path: str) -> None: 48 | """ 49 | Args: 50 | pair_path (str) 51 | """ 52 | pair_paths.append(pair_path) 53 | _update_modes() 54 | 55 | 56 | def windows_update_path() -> None: 57 | """ 58 | 1. Add the Apertium Binaries to shell PATH 59 | 2. Call apertium.append_pair_path for windows 60 | """ 61 | 62 | try: 63 | install_path = os.environ['LOCALAPPDATA'] 64 | current = os.environ['PATH'] 65 | 66 | apertium_bin_path = os.path.join(install_path, 'apertium-all-dev', 'bin') 67 | if os.path.isdir(apertium_bin_path): 68 | update_path = f'{current}{os.pathsep}{apertium_bin_path}{os.pathsep}' 69 | os.environ['PATH'] = update_path 70 | apertium_lang_path = \ 71 | os.path.join(install_path, 'apertium-all-dev', 'share', 'apertium') 72 | if os.path.isdir(apertium_lang_path): 73 | append_pair_path(apertium_lang_path) 74 | except KeyError: 75 | print('This function is available only for Windows') 76 | raise InstallationNotSupported(platform.system()) 77 | 78 | 79 | pair_paths = ['/usr/share/apertium', '/usr/local/share/apertium'] 80 | analyzers: Dict[str, Tuple[str, str]] = {} 81 | generators: Dict[str, Tuple[str, str]] = {} 82 | taggers: Dict[str, Tuple[str, str]] = {} 83 | pairs: Dict[str, str] = {} 84 | _update_modes() 85 | if platform.system() == 'Windows': 86 | windows_update_path() 87 | logging.basicConfig(format='[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s', level=logging.ERROR) 88 | logger = logging.getLogger() 89 | -------------------------------------------------------------------------------- /apertium/analysis/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List 3 | 4 | from streamparser import LexicalUnit, parse 5 | 6 | import apertium 7 | from apertium.utils import execute_pipeline, parse_mode_file, to_alpha3_code 8 | 9 | 10 | class Analyzer: 11 | """ 12 | Attributes: 13 | analyzer_cmds (Dict[str, List[List[str]]]) 14 | lang (str) 15 | """ 16 | 17 | def __init__(self, lang: str) -> None: 18 | """ 19 | Args: 20 | lang (str) 21 | """ 22 | self.analyzer_cmds: Dict[str, List[List[str]]] = {} 23 | self.lang: str = to_alpha3_code(lang) 24 | if self.lang not in apertium.analyzers: 25 | raise apertium.ModeNotInstalled(self.lang) 26 | else: 27 | self.path, self.mode = apertium.analyzers[self.lang] 28 | 29 | def _get_commands(self) -> List[List[str]]: 30 | """ 31 | Returns: 32 | List[List[str]] 33 | """ 34 | if self.lang not in self.analyzer_cmds: 35 | mode_path, mode = apertium.analyzers[self.lang] 36 | abs_mode_path = os.path.join(mode_path, 'modes', '{}.mode'.format(mode)) 37 | self.analyzer_cmds[self.lang] = parse_mode_file(abs_mode_path) 38 | 39 | return self.analyzer_cmds[self.lang] 40 | 41 | @staticmethod 42 | def _postproc_text(result: str) -> List[LexicalUnit]: 43 | """ 44 | Postprocesses the input 45 | 46 | Args: 47 | result (str) 48 | 49 | Returns: 50 | List[LexicalUnit] 51 | """ 52 | lexical_units: List[LexicalUnit] = list(parse(result)) 53 | return lexical_units 54 | 55 | def analyze(self, in_text: str, formatting: str = 'txt') -> List[LexicalUnit]: 56 | """ 57 | Runs apertium to analyze the input 58 | 59 | Args: 60 | in_text (str) 61 | formatting (str) 62 | 63 | Returns: 64 | List[LexicalUnit] 65 | """ 66 | self._get_commands() 67 | deformatter: List[str] = ['apertium-des{}'.format(formatting), '-n'] 68 | if deformatter not in self.analyzer_cmds[self.lang]: 69 | self.analyzer_cmds[self.lang].insert(0, deformatter) 70 | result: str = execute_pipeline(in_text, self.analyzer_cmds[self.lang]) 71 | return self._postproc_text(result) 72 | 73 | 74 | def analyze(lang: str, in_text: str, formatting: str = 'txt') -> List[LexicalUnit]: 75 | """ 76 | Args: 77 | lang (str) 78 | in_text (str) 79 | formatting (str) 80 | 81 | Returns: 82 | List[LexicalUnit] 83 | """ 84 | analyzer: Analyzer = Analyzer(lang) 85 | return analyzer.analyze(in_text, formatting) 86 | -------------------------------------------------------------------------------- /apertium/generation/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Union 2 | 3 | import apertium 4 | from apertium.utils import execute_pipeline, parse_mode_file, to_alpha3_code 5 | 6 | 7 | class Generator: 8 | """ 9 | Attributes: 10 | generation_cmds (Dict[str, List[List[str]]]) 11 | lang (str) 12 | """ 13 | 14 | def __init__(self, lang: str) -> None: 15 | """ 16 | Args: 17 | lang (str) 18 | """ 19 | self.generator_cmds: Dict[str, List[List[str]]] = {} 20 | self.lang: str = lang 21 | 22 | def _get_commands(self) -> List[List[str]]: 23 | """ 24 | Returns: 25 | List[List[str]] 26 | """ 27 | if self.lang not in self.generator_cmds: 28 | mode_path, mode = apertium.generators[self.lang] 29 | self.generator_cmds[self.lang] = parse_mode_file(mode_path + '/modes/' + mode + '.mode') 30 | return self.generator_cmds[self.lang] 31 | 32 | def generate(self, in_text: str, formatting: str = 'none') -> Union[str, List[str]]: 33 | """ 34 | Args: 35 | in_text (str) 36 | formatting (str) 37 | 38 | Returns: 39 | Union[str, List[str]] 40 | """ 41 | self.lang = to_alpha3_code(self.lang) 42 | 43 | if self.lang in apertium.generators: 44 | commands = list(self._get_commands()) 45 | result = execute_pipeline(in_text, commands) 46 | return result.rstrip('\x00') 47 | else: 48 | raise apertium.ModeNotInstalled(self.lang) 49 | 50 | 51 | def generate(lang: str, in_text: str, formatting: str = 'none') -> Union[str, List[str]]: 52 | """ 53 | Args: 54 | lang (str) 55 | in_text (str) 56 | formatting (str) 57 | 58 | Returns: 59 | Union[str, List[str]] 60 | """ 61 | generator = Generator(lang) 62 | return generator.generate(in_text, formatting) 63 | -------------------------------------------------------------------------------- /apertium/installer.py: -------------------------------------------------------------------------------- 1 | from distutils.dir_util import copy_tree 2 | import logging 3 | import os 4 | import platform 5 | import re 6 | import shutil 7 | import subprocess 8 | import tempfile 9 | from typing import Dict, Optional, Union 10 | from urllib.request import urlretrieve 11 | from zipfile import ZipFile 12 | 13 | import apertium 14 | 15 | nightly: bool = True 16 | 17 | 18 | class Windows: 19 | """Download ApertiumWin64 and Move to %localappdata%""" 20 | base_link = 'http://apertium.projectjj.com/{}' 21 | 22 | def __init__(self) -> None: 23 | self._install_path: str = str(os.getenv('LOCALAPPDATA')) 24 | self._apertium_path: str = str(os.path.join(self._install_path, 'apertium-all-dev')) 25 | self._download_path = tempfile.mkdtemp() 26 | logging.basicConfig(format='%(asctime)s %(message)s', level=logging.DEBUG) 27 | self._logger = logging.getLogger() 28 | self._logger.setLevel(logging.DEBUG) 29 | 30 | def _download_zip(self, download_files: Dict[str, str], extract_path: Optional[str]) -> None: 31 | for zip_name, zip_link in download_files.items(): 32 | zip_download_path = os.path.join(self._download_path, zip_name) 33 | urlretrieve(Windows.base_link.format(zip_link), filename=zip_download_path) 34 | self._logger.info('%s -> %s download completed', Windows.base_link.format(zip_link), zip_name) 35 | 36 | # Extract the zip 37 | with ZipFile(zip_download_path) as zip_file: 38 | zip_file.extractall(path=extract_path) 39 | self._logger.info('%s Extraction completed', zip_name) 40 | os.remove(zip_download_path) 41 | self._logger.info('%s removed', zip_name) 42 | 43 | def _download_package(self, package: str) -> None: 44 | """Installs Packages to %localappdata%/Apertium""" 45 | 46 | install_sh = 'nightly' if nightly else 'release' 47 | zip_path = f'win32/{install_sh}/data.php?zip=' 48 | package_zip = {package: zip_path + package} 49 | self._download_zip(package_zip, self._download_path) 50 | 51 | # move the extracted files to desired location 52 | lang_data_path = os.path.join(self._download_path, 'usr', 'share', 'apertium') 53 | 54 | self._logger.info('Copying Language Data to Apertium') 55 | for directory in os.listdir(lang_data_path): 56 | source: str = str(os.path.join(lang_data_path, directory)) 57 | destination: str = str(os.path.join(self._apertium_path, 'share', 'apertium', directory)) 58 | copy_tree(source, destination) 59 | self._logger.info('%s -> %s', source, destination) 60 | 61 | shutil.rmtree(os.path.join(self._download_path, 'usr')) 62 | 63 | def _edit_modes(self) -> None: 64 | r"""The mode files need to be modified before being used on Windows System 65 | 66 | 1. Replace /usr/share with %localappdata%\apertium-all-dev\share 67 | 2. Replace "/" with "\" to make path compatible with Windows System 68 | """ 69 | 70 | # List of Mode Files 71 | mode_path: str = str(os.path.join(self._apertium_path, 'share', 'apertium', 'modes')) 72 | for f in os.listdir(mode_path): 73 | if os.path.isfile(os.path.join(mode_path, f)) and f.endswith('.mode'): 74 | self._logger.info('Editing mode %s ', f) 75 | with open(os.path.join(mode_path, f)) as infile: 76 | line = infile.read() 77 | 78 | contents = line.split(' ') 79 | # Editing mode file to be compatible with windows platform 80 | for i, t in enumerate(contents): 81 | if len(t) > 2 and t[0] == "'" and t[1] == '/': 82 | t = t.replace('/', os.sep) 83 | t = t.replace(r'\usr', self._apertium_path) 84 | contents[i] = t 85 | line = ' '.join(contents) 86 | with open(os.path.join(mode_path, f), 'w') as outfile: 87 | outfile.write(line) 88 | outfile.close() 89 | 90 | def install_apertium_base(self) -> None: 91 | """Installs Apertium-all-dev to %localappdata%""" 92 | 93 | apertium_windows = { 94 | 'apertium-all-dev.zip': '/win64/nightly/apertium-all-dev.zip', 95 | } 96 | 97 | self._download_zip(apertium_windows, self._install_path) 98 | 99 | def install_apertium_module(self, language: str) -> None: 100 | self._download_package(language) 101 | self._edit_modes() 102 | 103 | def install_wrapper(self, swig_wrapper: str) -> None: 104 | # TODO: create installer for wrappers on windows 105 | pass 106 | 107 | 108 | class Debian: 109 | @staticmethod 110 | def _install_package_source() -> None: 111 | install_sh = 'install-nightly.sh' if nightly else 'install-release.sh' 112 | install_script_url = f'http://apertium.projectjj.com/apt/{install_sh}' 113 | with tempfile.NamedTemporaryFile('w') as install_script: 114 | urlretrieve(install_script_url, install_script.name) 115 | execute = subprocess.run(['sudo', 'bash', install_script.name]) 116 | execute.check_returncode() 117 | 118 | @staticmethod 119 | def _download_package(package: str) -> None: 120 | command = ['sudo', 'apt-get', 'install', '-y', package] 121 | execute = subprocess.run(command) 122 | execute.check_returncode() 123 | 124 | @staticmethod 125 | def _rename_wrappers() -> None: 126 | wrapper_name = { 127 | 'python3-apertium-core': '_apertium_core', 128 | 'python3-apertium-lex-tools': '_apertium_lex_tools', 129 | 'python3-cg3': '_constraint_grammar', 130 | 'python3-lttoolbox': '_lttoolbox', 131 | } 132 | dist_package = '/usr/lib/python3/dist-packages' 133 | for wrapper in wrapper_name.values(): 134 | for f in os.listdir(dist_package): 135 | if f.startswith(wrapper): 136 | old_name = os.path.join(dist_package, f) 137 | new_name = os.path.join(dist_package, '{}.so'.format(f.split('.')[0])) 138 | if old_name != new_name: 139 | subprocess.run(['sudo', 'mv', old_name, new_name]) 140 | 141 | def install_apertium_module(self, language: str) -> None: 142 | self._download_package(language) 143 | 144 | def install_apertium_base(self) -> None: 145 | self._install_package_source() 146 | self._download_package('apertium-all-dev') 147 | 148 | def install_wrapper(self, swig_wrapper: str) -> None: 149 | self._download_package(swig_wrapper) 150 | self._rename_wrappers() 151 | 152 | 153 | def get_installer() -> Union[Windows, Debian]: 154 | system: str = platform.system() 155 | if system == 'Windows': 156 | return Windows() 157 | elif system == 'Linux': 158 | with open('/etc/os-release') as os_release: 159 | distro_name = os_release.read() 160 | if re.search('[Dd]ebian|[Uu]buntu', distro_name) is not None: 161 | return Debian() 162 | else: 163 | raise apertium.InstallationNotSupported(distro_name) 164 | else: 165 | raise apertium.InstallationNotSupported(system) 166 | 167 | 168 | def install_apertium() -> None: 169 | installer = get_installer() 170 | installer.install_apertium_base() 171 | 172 | 173 | def install_module(module: str) -> None: 174 | apertium_module = 'apertium-{}'.format(module) 175 | installer: Union[Windows, Debian] = get_installer() 176 | installer.install_apertium_module(apertium_module) 177 | apertium._update_modes() 178 | 179 | 180 | def install_wrapper(swig_wrapper: str) -> None: 181 | installer: Union[Windows, Debian] = get_installer() 182 | installer.install_wrapper(swig_wrapper) 183 | 184 | 185 | def install_apertium_linux() -> None: 186 | """ 187 | Installs apertium-* packages on Linux Platforms 188 | """ 189 | if platform.system() == 'Linux': 190 | install_module('anaphora') 191 | -------------------------------------------------------------------------------- /apertium/iso639.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | iso_639_codes: Dict[str, str] = { 4 | 'aar': 'aa', 5 | 'abk': 'ab', 6 | 'afr': 'af', 7 | 'aka': 'ak', 8 | 'amh': 'am', 9 | 'ara': 'ar', 10 | 'arg': 'an', 11 | 'asm': 'as', 12 | 'ava': 'av', 13 | 'ave': 'ae', 14 | 'aym': 'ay', 15 | 'azb': 'az', 16 | 'aze': 'az', 17 | 'bak': 'ba', 18 | 'bam': 'bm', 19 | 'bel': 'be', 20 | 'ben': 'bn', 21 | 'bih': 'bh', 22 | 'bis': 'bi', 23 | 'bod': 'bo', 24 | 'bos': 'bs', 25 | 'bre': 'br', 26 | 'bul': 'bg', 27 | 'cat': 'ca', 28 | 'ces': 'cs', 29 | 'cha': 'ch', 30 | 'che': 'ce', 31 | 'chu': 'cu', 32 | 'chv': 'cv', 33 | 'cor': 'kw', 34 | 'cos': 'co', 35 | 'cre': 'cr', 36 | 'cym': 'cy', 37 | 'dan': 'da', 38 | 'deu': 'de', 39 | 'div': 'dv', 40 | 'dzo': 'dz', 41 | 'ell': 'el', 42 | 'eng': 'en', 43 | 'epo': 'eo', 44 | 'est': 'et', 45 | 'eus': 'eu', 46 | 'ewe': 'ee', 47 | 'fao': 'fo', 48 | 'fas': 'fa', 49 | 'fij': 'fj', 50 | 'fin': 'fi', 51 | 'fra': 'fr', 52 | 'fry': 'fy', 53 | 'ful': 'ff', 54 | 'gla': 'gd', 55 | 'gle': 'ga', 56 | 'glg': 'gl', 57 | 'glv': 'gv', 58 | 'grn': 'gn', 59 | 'guj': 'gu', 60 | 'hat': 'ht', 61 | 'hau': 'ha', 62 | 'hbs': 'sh', 63 | 'heb': 'he', 64 | 'her': 'hz', 65 | 'hin': 'hi', 66 | 'hmo': 'ho', 67 | 'hrv': 'hr', 68 | 'hun': 'hu', 69 | 'hye': 'hy', 70 | 'ibo': 'ig', 71 | 'ido': 'io', 72 | 'iii': 'ii', 73 | 'iku': 'iu', 74 | 'ile': 'ie', 75 | 'ina': 'ia', 76 | 'ind': 'id', 77 | 'ipk': 'ik', 78 | 'isl': 'is', 79 | 'ita': 'it', 80 | 'jav': 'jv', 81 | 'jpn': 'ja', 82 | 'kal': 'kl', 83 | 'kan': 'kn', 84 | 'kas': 'ks', 85 | 'kat': 'ka', 86 | 'kau': 'kr', 87 | 'kaz': 'kk', 88 | 'khm': 'km', 89 | 'kik': 'ki', 90 | 'kin': 'rw', 91 | 'kir': 'ky', 92 | 'kom': 'kv', 93 | 'kon': 'kg', 94 | 'kor': 'ko', 95 | 'kua': 'kj', 96 | 'kur': 'ku', 97 | 'lao': 'lo', 98 | 'lat': 'la', 99 | 'lav': 'lv', 100 | 'lim': 'li', 101 | 'lin': 'ln', 102 | 'lit': 'lt', 103 | 'ltz': 'lb', 104 | 'lub': 'lu', 105 | 'lug': 'lg', 106 | 'mah': 'mh', 107 | 'mal': 'ml', 108 | 'mar': 'mr', 109 | 'mkd': 'mk', 110 | 'mlg': 'mg', 111 | 'mlt': 'mt', 112 | 'mon': 'mn', 113 | 'mri': 'mi', 114 | 'msa': 'ms', 115 | 'mya': 'my', 116 | 'nau': 'na', 117 | 'nav': 'nv', 118 | 'nbl': 'nr', 119 | 'nde': 'nd', 120 | 'ndo': 'ng', 121 | 'nep': 'ne', 122 | 'nld': 'nl', 123 | 'nno': 'nn', 124 | 'nob': 'nb', 125 | 'nor': 'no', 126 | 'nya': 'ny', 127 | 'oci': 'oc', 128 | 'oji': 'oj', 129 | 'ori': 'or', 130 | 'orm': 'om', 131 | 'oss': 'os', 132 | 'pan': 'pa', 133 | 'pes': 'fa', 134 | 'pli': 'pi', 135 | 'pol': 'pl', 136 | 'por': 'pt', 137 | 'pus': 'ps', 138 | 'que': 'qu', 139 | 'roh': 'rm', 140 | 'ron': 'ro', 141 | 'run': 'rn', 142 | 'rus': 'ru', 143 | 'sag': 'sg', 144 | 'san': 'sa', 145 | 'sin': 'si', 146 | 'slk': 'sk', 147 | 'slv': 'sl', 148 | 'sme': 'se', 149 | 'smo': 'sm', 150 | 'sna': 'sn', 151 | 'snd': 'sd', 152 | 'som': 'so', 153 | 'sot': 'st', 154 | 'spa': 'es', 155 | 'sqi': 'sq', 156 | 'srd': 'sc', 157 | 'srp': 'sr', 158 | 'ssw': 'ss', 159 | 'sun': 'su', 160 | 'swa': 'sw', 161 | 'swe': 'sv', 162 | 'tah': 'ty', 163 | 'tam': 'ta', 164 | 'tat': 'tt', 165 | 'tel': 'te', 166 | 'tgk': 'tg', 167 | 'tgl': 'tl', 168 | 'tha': 'th', 169 | 'tir': 'ti', 170 | 'ton': 'to', 171 | 'tsn': 'tn', 172 | 'tso': 'ts', 173 | 'tuk': 'tk', 174 | 'tur': 'tr', 175 | 'twi': 'tw', 176 | 'uig': 'ug', 177 | 'ukr': 'uk', 178 | 'urd': 'ur', 179 | 'uzb': 'uz', 180 | 'ven': 've', 181 | 'vie': 'vi', 182 | 'vol': 'vo', 183 | 'wln': 'wa', 184 | 'wol': 'wo', 185 | 'xho': 'xh', 186 | 'yid': 'yi', 187 | 'yor': 'yo', 188 | 'zha': 'za', 189 | 'zho': 'zh', 190 | 'zul': 'zu', 191 | } 192 | -------------------------------------------------------------------------------- /apertium/mode_search.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from typing import Dict, List, Pattern, Tuple, Union 4 | 5 | from apertium.utils import to_alpha3_code 6 | 7 | 8 | def is_loop(dirpath: str, rootpath: str, real_root: Union[None, str] = None) -> bool: 9 | """ 10 | Args: 11 | dirpath (str) 12 | rootpath (str) 13 | real_root (Union[None, str]) 14 | 15 | Returns: 16 | bool 17 | """ 18 | if os.path.islink(dirpath): 19 | # We just descended into a directory via a symbolic link 20 | # Check if we're referring to a directory that is 21 | # a parent of our nominal directory 22 | 23 | if not real_root: 24 | real_root = os.path.abspath(os.path.realpath(rootpath)) 25 | 26 | relative = os.path.relpath(dirpath, rootpath) 27 | nominal_path = os.path.join(real_root, relative) 28 | real_path = os.path.abspath(os.path.realpath(dirpath)) 29 | 30 | for nominal, real in zip(nominal_path.split(os.sep), 31 | real_path.split(os.sep)): 32 | if nominal != real: 33 | return False 34 | else: 35 | return True 36 | else: 37 | return False 38 | 39 | 40 | def search_path(rootpath: str, include_pairs: bool = True) -> Dict[str, List[Tuple[str, str, str]]]: 41 | """ 42 | Args: 43 | rootpath (str) 44 | include_pairs (bool) 45 | 46 | Returns: 47 | Dict[str, List[Tuple[str, str, str]]] 48 | """ 49 | lang_code: str = r'[a-z]{2,3}(?:_[A-Za-z]+)?' 50 | type_re: Dict[str, Pattern[str]] = { 51 | 'analyzer': re.compile(r'(({0}(-{0})?)-(an)?mor(ph)?)\.mode'.format(lang_code)), 52 | 'generator': re.compile(r'(({0}(-{0})?)-gener[A-z]*)\.mode'.format(lang_code)), 53 | 'pair': re.compile(r'({0})-({0})\.mode'.format(lang_code)), 54 | 'tagger': re.compile(r'(({0}(-{0})?)-tagger[A-z]*)\.mode'.format(lang_code)), 55 | } 56 | modes: Dict[str, List[Tuple[str, str, str]]] = { 57 | 'analyzer': [], 58 | 'generator': [], 59 | 'pair': [], 60 | 'tagger': [], 61 | } 62 | 63 | real_root = os.path.abspath(os.path.realpath(rootpath)) 64 | 65 | for dirpath, dirnames, files in os.walk(rootpath, followlinks=True): 66 | if is_loop(dirpath, rootpath, real_root): 67 | dirnames[:] = [] 68 | continue 69 | for filename in [f for f in files if f.endswith('.mode')]: 70 | for mtype, regex in type_re.items(): 71 | m = regex.match(filename) 72 | if m: 73 | if mtype != 'pair': 74 | modename = m.group(1) # e.g. en-es-anmorph 75 | langlist = [to_alpha3_code(l) for l in m.group(2).split('-')] 76 | lang_pair = '-'.join(langlist) # e.g. en-es 77 | dir_of_modes = os.path.dirname(dirpath) 78 | mode = (dir_of_modes, 79 | modename, 80 | lang_pair) 81 | modes[mtype].append(mode) 82 | elif include_pairs: 83 | lang_src, lang_trg = m.groups() 84 | mode = (os.path.join(dirpath, filename), 85 | to_alpha3_code(lang_src), 86 | to_alpha3_code(lang_trg)) 87 | modes[mtype].append(mode) 88 | return modes 89 | -------------------------------------------------------------------------------- /apertium/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apertium/apertium-python/81b10e509f65fcf1c77a0c2080f398897d3629c2/apertium/py.typed -------------------------------------------------------------------------------- /apertium/tagger/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List 3 | 4 | from streamparser import LexicalUnit, parse 5 | 6 | import apertium 7 | from apertium.utils import execute_pipeline, parse_mode_file, to_alpha3_code 8 | 9 | 10 | class Tagger: 11 | """ 12 | Attributes: 13 | tagger_cmds (Dict[str, List[List[str]]]) 14 | lang (str) 15 | """ 16 | 17 | def __init__(self, lang: str) -> None: 18 | """ 19 | Args: 20 | lang (str) 21 | """ 22 | self.tagger_cmds: Dict[str, List[List[str]]] = {} 23 | self.lang: str = to_alpha3_code(lang) 24 | if self.lang not in apertium.taggers: 25 | raise apertium.ModeNotInstalled(self.lang) 26 | else: 27 | self.path, self.mode = apertium.taggers[self.lang] 28 | 29 | def _get_commands(self) -> List[List[str]]: 30 | """ 31 | Returns: 32 | List[List[str]] 33 | """ 34 | if self.lang not in self.tagger_cmds: 35 | mode_path, mode = apertium.taggers[self.lang] 36 | abs_mode_path = os.path.join(mode_path, 'modes', '{}.mode'.format(mode)) 37 | self.tagger_cmds[self.lang] = parse_mode_file(abs_mode_path) 38 | 39 | return self.tagger_cmds[self.lang] 40 | 41 | @staticmethod 42 | def _postproc_text(result: str) -> List[LexicalUnit]: 43 | """ 44 | Postprocesses the input 45 | 46 | Args: 47 | result (str) 48 | 49 | Returns: 50 | List[LexicalUnit] 51 | """ 52 | lexical_units = list(parse(result)) 53 | return lexical_units 54 | 55 | def tag(self, in_text: str, formatting: str = 'txt') -> List[LexicalUnit]: 56 | """ 57 | Runs apertium to tagger the input 58 | 59 | Args: 60 | in_text (str) 61 | formatting (str) 62 | 63 | Returns: 64 | List[LexicalUnit] 65 | """ 66 | self._get_commands() 67 | deformatter = ['apertium-des{}'.format(formatting), '-n'] 68 | if deformatter not in self.tagger_cmds[self.lang]: 69 | self.tagger_cmds[self.lang].insert(0, deformatter) 70 | result = execute_pipeline(in_text, self.tagger_cmds[self.lang]) 71 | return self._postproc_text(result) 72 | 73 | 74 | def tag(lang: str, in_text: str, formatting: str = 'txt') -> List[LexicalUnit]: 75 | """ 76 | Args: 77 | lang (str) 78 | in_text (str) 79 | formatting (str) 80 | 81 | Returns: 82 | List[LexicalUnit] 83 | """ 84 | tagger = Tagger(lang) 85 | return tagger.tag(in_text, formatting) 86 | -------------------------------------------------------------------------------- /apertium/translation/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | from subprocess import CalledProcessError, PIPE, Popen 3 | from typing import Dict, List, Optional, Tuple, Union 4 | 5 | import apertium # noqa: E402 6 | from apertium.utils import execute_pipeline, parse_mode_file, to_alpha3_code # noqa: E402 7 | 8 | 9 | class Translator: 10 | """ 11 | Attributes: 12 | translation_cmds (Dict[Tuple[str, str], List[List[str]]]) 13 | lang1 (str) 14 | lang2 (str) 15 | """ 16 | 17 | def __init__(self, lang1: str, lang2: str) -> None: 18 | """ 19 | Args: 20 | lang1 (str) 21 | lang2 (str) 22 | """ 23 | self.translation_cmds: Dict[Tuple[str, str], List[List[str]]] = {} 24 | self.lang1 = lang1 25 | self.lang2 = lang2 26 | 27 | def _get_commands(self, lang1: str, lang2: str) -> List[List[str]]: 28 | """ 29 | Args: 30 | lang1 (str) 31 | lang2 (str) 32 | 33 | Returns: 34 | List[List[str]] 35 | """ 36 | if (lang1, lang2) not in self.translation_cmds: 37 | mode_path = apertium.pairs['%s-%s' % (lang1, lang2)] 38 | self.translation_cmds[(lang1, lang2)] = parse_mode_file(mode_path) 39 | return self.translation_cmds[(lang1, lang2)] 40 | 41 | def _get_format(self, formatting: Optional[str], deformat: Optional[str], reformat: Optional[str]) -> Tuple[Optional[str], Optional[str]]: 42 | """ 43 | Args: 44 | formatting (Optional[str]) 45 | deformat (Optional[str]) 46 | reformat (Optional[str]) 47 | 48 | Returns: 49 | Tuple[Optional[str], Optional[str]] 50 | """ 51 | if formatting: 52 | deformat = 'apertium-des' + formatting 53 | reformat = 'apertium-re' + formatting 54 | else: 55 | if 'apertium-des' not in deformat: # type: ignore 56 | deformat = 'apertium-des' + deformat # type: ignore 57 | if 'apertium-re' not in reformat: # type: ignore 58 | reformat = 'apertium-re' + reformat # type: ignore 59 | 60 | return deformat, reformat 61 | 62 | def _check_ret_code(self, proc: Popen, cmd: str) -> None: 63 | """ 64 | Args: 65 | proc (Popen) 66 | cmd (str) 67 | """ 68 | if proc.returncode != 0: 69 | raise CalledProcessError(proc.returncode, cmd) 70 | 71 | def _validate_formatters(self, deformat: Optional[str], reformat: Optional[str]) -> Tuple[Union[str, bool], Union[str, bool]]: 72 | """ 73 | Args: 74 | deformat (Optional[str]) 75 | reformat (Optional[str]) 76 | 77 | Returns: 78 | Tuple[Union[str, bool], Union[str, bool]] 79 | """ 80 | def valid1(elt: Optional[str], lst: List[Union[str, bool]]) -> Union[str, bool]: 81 | """ 82 | Args: 83 | elt (Optional[str]) 84 | lst (List[Union[str, bool]]) 85 | 86 | Returns: 87 | Union[str, bool] 88 | """ 89 | if elt in lst: 90 | return elt 91 | else: 92 | return lst[0] 93 | 94 | # First is fallback: 95 | deformatters: List[Union[str, bool]] = [ 96 | 'apertium-deshtml', 97 | 'apertium-destxt', 98 | 'apertium-desrtf', 99 | False, 100 | ] 101 | reformatters: List[Union[str, bool]] = [ 102 | 'apertium-rehtml-noent', 103 | 'apertium-rehtml', 104 | 'apertium-retxt', 105 | 'apertium-rertf', 106 | False, 107 | ] 108 | return valid1(deformat, deformatters), valid1(reformat, reformatters) 109 | 110 | def _get_deformat(self, deformat: str, text: str) -> str: 111 | """ 112 | Args: 113 | deformat (str) 114 | text (str) 115 | 116 | Returns: 117 | str 118 | """ 119 | if deformat: 120 | proc_deformat = Popen(deformat, stdin=PIPE, stdout=PIPE) 121 | proc_deformat.stdin.write(bytes(text, 'utf-8')) 122 | deformatted = proc_deformat.communicate()[0] 123 | deformatted = deformatted.decode() 124 | self._check_ret_code(proc_deformat, deformat) 125 | else: 126 | deformatted = bytes(text, 'utf-8') 127 | res = str(deformatted) 128 | return res 129 | 130 | def _get_reformat(self, reformat: str, text: str) -> bytes: 131 | """ 132 | Args: 133 | reformat (str) 134 | text (str) 135 | 136 | Returns: 137 | str 138 | """ 139 | result: bytes 140 | if reformat: 141 | proc_reformat = Popen(reformat, stdin=PIPE, stdout=PIPE) 142 | proc_reformat.stdin.write(bytes(text, 'utf-8')) 143 | result = proc_reformat.communicate()[0] 144 | self._check_ret_code(proc_reformat, reformat) 145 | else: 146 | result = re.sub(rb'\0$', b'', text) # type: ignore 147 | return result 148 | 149 | def translate(self, text: str, mark_unknown: bool = False, formatting: Optional[str] = None, deformat: str = 'txt', reformat: str = 'txt') -> str: 150 | """ 151 | Args: 152 | text (str) 153 | mark_unknown (bool) 154 | formatting (Optional[str]) 155 | deformat (str) 156 | reformat (str) 157 | 158 | Returns: 159 | str 160 | """ 161 | if '{}-{}'.format(*map(to_alpha3_code, [self.lang1, self.lang2])) in apertium.pairs: 162 | pair = map(to_alpha3_code, [self.lang1, self.lang2]) 163 | else: 164 | raise apertium.ModeNotInstalled() 165 | 166 | if pair is not None: 167 | lang1, lang2 = pair 168 | cmds = list(self._get_commands(lang1, lang2)) 169 | unsafe_deformat, unsafe_reformat = self._get_format(formatting, deformat, reformat) 170 | deformater, reformater = self._validate_formatters(unsafe_deformat, unsafe_reformat) 171 | deformatted = self._get_deformat(str(deformater), text) 172 | output = execute_pipeline(deformatted, cmds) 173 | result: bytes = self._get_reformat(str(reformater), output).strip() 174 | return result.decode() 175 | 176 | 177 | def translate(lang1: str, lang2: str, text: str, mark_unknown: bool = False, 178 | formatting: Optional[str] = None, deformat: str = 'txt', reformat: str = 'txt') -> str: 179 | """ 180 | Args: 181 | lang1: str 182 | lang2: str 183 | text (str) 184 | mark_unknown (bool) 185 | formatting (Optional[str]) 186 | deformat (str) 187 | reformat (str) 188 | 189 | Returns: 190 | str 191 | """ 192 | translator = Translator(lang1, lang2) 193 | return translator.translate(text, mark_unknown, formatting, deformat, reformat) 194 | -------------------------------------------------------------------------------- /apertium/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import subprocess 4 | import sys 5 | import tempfile 6 | from typing import Any, Dict, List, Tuple, Union 7 | 8 | try: 9 | if platform.system() == 'Linux': 10 | sys.path.append('/usr/lib/python3/dist-packages') 11 | 12 | import apertium_core 13 | import apertium_lex_tools 14 | import lttoolbox 15 | import constraint_grammar 16 | wrappers_available = True 17 | except ImportError: 18 | wrappers_available = False 19 | 20 | import apertium # noqa: F401 21 | from apertium.iso639 import iso_639_codes 22 | 23 | iso639_codes_inverse = {v: k for k, v in iso_639_codes.items()} 24 | escape_chars = b'[]{}?^$@\\' 25 | special_chars_map = {i: '\\' + chr(i) for i in escape_chars} 26 | initialized_wrappers = {} # type: Dict[Union[List[str], Tuple[str, ...]], Union[FSTProc, LRX, Any]] 27 | 28 | if wrappers_available: 29 | class FSTProc(lttoolbox.FST): # type: ignore 30 | def __init__(self, dictionary_path: str, arg: str) -> None: 31 | super().__init__(dictionary_path) 32 | if arg == '-g': 33 | self.initGeneration() 34 | elif arg == '-b': 35 | self.initBiltrans() 36 | elif arg == '-p': 37 | self.initPostgeneration() 38 | else: 39 | self.initAnalysis() 40 | 41 | class LRX(apertium_lex_tools.LRXProc): # type: ignore 42 | def __init__(self, dictionary_path: str) -> None: 43 | super().__init__(dictionary_path) 44 | self.init() 45 | 46 | 47 | def to_alpha3_code(code: str) -> str: 48 | """ 49 | Args: 50 | code (str) 51 | 52 | Returns: 53 | str 54 | """ 55 | if '_' in code: 56 | code, variant = code.split('_') 57 | return '%s_%s' % ((iso639_codes_inverse[code], variant) if code in iso639_codes_inverse else (code, variant)) 58 | else: 59 | return iso639_codes_inverse[code] if code in iso639_codes_inverse else code 60 | 61 | 62 | def deformatter(text: str) -> str: 63 | """ 64 | This function is a text format processor. Data should be passed 65 | through this processor before being piped to lt-proc. 66 | """ 67 | return '{}[][\n]'.format(text.translate(special_chars_map)) 68 | 69 | 70 | def handle_command_with_wrapper(command: Union[List[str], Tuple[str, ...]], end: bytes) -> Tuple[bytes, bool]: 71 | """ 72 | Executes the given command via wrappers 73 | """ 74 | used_wrapper = True 75 | if command not in initialized_wrappers.keys() and wrappers_available: 76 | if 'lt-proc' == command[0]: 77 | lt_proc_command, dictionary_path = command[:-1], command[-1] 78 | arg = command[1] 79 | lttoolbox.LtLocale.tryToSetLocale() 80 | fst = FSTProc(dictionary_path, arg) # type: FSTProc 81 | if not fst.valid(): 82 | raise ValueError('FST Invalid') 83 | initialized_wrappers[command] = fst 84 | elif 'lrx-proc' == command[0]: 85 | dictionary_path = command[-1] 86 | apertium_lex_tools.LtLocale.tryToSetLocale() 87 | lrx = LRX(dictionary_path) # type: LRX 88 | initialized_wrappers[command] = lrx 89 | elif 'apertium-transfer' == command[0]: 90 | transfer = apertium_core.ApertiumTransfer(command[-2], command[-1]) 91 | initialized_wrappers[command] = transfer 92 | elif 'apertium-interchunk' == command[0]: 93 | interchunk = apertium_core.ApertiumInterchunk(command[-2], command[-1]) 94 | initialized_wrappers[command] = interchunk 95 | elif 'apertium-postchunk' == command[0]: 96 | postchunk = apertium_core.ApertiumPostchunk(command[-2], command[-1]) 97 | initialized_wrappers[command] = postchunk 98 | elif 'cg-proc' == command[0]: 99 | dictionary_path = command[-1] 100 | cg = constraint_grammar.CGProc(dictionary_path) 101 | initialized_wrappers[command] = cg 102 | 103 | input_file = tempfile.NamedTemporaryFile(delete=False, mode='w') 104 | output_file = tempfile.NamedTemporaryFile(delete=False) 105 | 106 | text = end.decode() 107 | input_file.write(text) 108 | input_file.close() 109 | 110 | if 'lt-proc' == command[0]: 111 | fst = initialized_wrappers[command] 112 | lt_proc_command, dictionary_path, arg = command[:-1], command[-1], command[1] 113 | fst.lt_proc(lt_proc_command, input_file.name, output_file.name) 114 | elif 'lrx-proc' == command[0]: 115 | apertium_lex_tools.LtLocale.tryToSetLocale() 116 | lrx = initialized_wrappers[command] 117 | lrx.lrx_proc(command, input_file.name, output_file.name) 118 | elif 'apertium-transfer' == command[0]: 119 | transfer = initialized_wrappers[command] 120 | transfer.transfer_text(command, input_file.name, output_file.name) 121 | elif 'apertium-interchunk' == command[0]: 122 | interchunk = initialized_wrappers[command] 123 | interchunk.interchunk_text(command, input_file.name, output_file.name) 124 | elif 'apertium-postchunk' == command[0]: 125 | postchunk = initialized_wrappers[command] 126 | postchunk.postchunk_text(command, input_file.name, output_file.name) 127 | elif 'apertium-pretransfer' == command[0]: 128 | apertium_core.pretransfer(command, input_file.name, output_file.name) 129 | elif 'apertium-tagger' == command[0]: 130 | command = list(command) 131 | command += [input_file.name, output_file.name] 132 | tuple_command = tuple(command) 133 | apertium_core.ApertiumTagger(tuple_command) 134 | elif 'cg-proc' == command[0]: 135 | cg = initialized_wrappers[command] 136 | cg.cg_proc(command, input_file.name, output_file.name) 137 | else: 138 | used_wrapper = False 139 | 140 | if used_wrapper: 141 | output_file.seek(0) 142 | end = output_file.read() 143 | output_file.close() 144 | 145 | os.remove(input_file.name) 146 | os.remove(output_file.name) 147 | 148 | return end, used_wrapper 149 | 150 | 151 | def execute_pipeline(inp: str, commands: List[List[str]]) -> str: 152 | """ 153 | Executes the given list of commands and returns the final output 154 | 155 | Returns: 156 | str 157 | """ 158 | end = inp.encode() 159 | for command in commands: 160 | # On Windows, a NamedTemporaryFile with delete=True can only be opened once. 161 | # Since the file is opened both by Python and the C++ SWIG wrappers, we use 162 | # delete=False and manually delete the file. 163 | used_wrapper = True 164 | tuple_command = tuple(command) # type: Tuple[str, ...] 165 | if 'apertium-destxt' == command[0]: 166 | output = deformatter(end.decode()) 167 | end = output.encode() 168 | continue 169 | 170 | if wrappers_available: 171 | end, used_wrapper = handle_command_with_wrapper(tuple_command, end) 172 | if not wrappers_available or not used_wrapper: 173 | apertium.logger.warning('Calling subprocess %s', command[0]) 174 | proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 175 | end, _ = proc.communicate(end) 176 | return end.decode() 177 | 178 | 179 | def parse_mode_file(mode_path: str) -> List[List[str]]: 180 | """ 181 | Args: 182 | mode_path (str) 183 | 184 | Returns: 185 | List[List[str]] 186 | """ 187 | with open(mode_path) as mode_file: 188 | mode_str = mode_file.read().strip() 189 | if mode_str: 190 | commands = [] 191 | for cmd in mode_str.strip().split('|'): 192 | # TODO: we should make language pairs install 193 | # modes.xml instead; this is brittle (what if a path 194 | # has | or ' in it?) 195 | cmd = cmd.replace('$2', '').replace('$1', '-g') 196 | commands.append([c.strip("'") for c in cmd.split()]) 197 | return commands 198 | else: 199 | raise apertium.ModeNotInstalled(mode_path) 200 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/source/about.md: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | Introduction 5 | ------------ 6 | 7 | - The code-base is in development for the GSoC '19 project called **Apertium API in Python**. 8 | - The Apertium core modules are written in C++. 9 | - The existing codebase has `Subprocess` and [SWIG](http://www.swig.org/) wrapper implementations of the higher level functions of Apertium and CG modules. 10 | 11 | About the Existing Code Base 12 | ----------------------------- 13 | 14 | - The existing codebase has `Subprocess` and SWIG wrapper implementations of the basic functions used by Apertium modules. 15 | 16 | Contributing 17 | ------------ 18 | 19 | - Issue Tracker: 20 | - Source Code: 21 | - Run unit tests with `./setup.py test`. 22 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from recommonmark.parser import CommonMarkParser 5 | from recommonmark.transform import AutoStructify 6 | 7 | sys.path.insert(0, os.path.abspath('../..')) 8 | 9 | 10 | # -- Project information ----------------------------------------------------- 11 | 12 | project = 'apertium-python' 13 | copyright = '2018, Andi Qu' 14 | author = 'Andi Qu' 15 | 16 | # The short X.Y version 17 | version = '' 18 | # The full version, including alpha/beta/rc tags 19 | release = '' 20 | 21 | extensions = [ 22 | 'sphinx.ext.autodoc', 23 | 'sphinx.ext.intersphinx', 24 | 'sphinx.ext.viewcode', 25 | 'sphinx.ext.githubpages', 26 | 'sphinx.ext.napoleon', 27 | ] 28 | napoleon_include_special_with_doc = False 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['ntemplates'] 32 | 33 | # The suffix(es) of source filenames. 34 | # You can specify multiple suffix as a list of string: 35 | # 36 | 37 | source_parsers = { 38 | '.md': CommonMarkParser, 39 | } 40 | 41 | source_suffix = ['.rst', '.md'] 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # The language for content autogenerated by Sphinx. Refer to documentation 47 | # for a list of supported languages. 48 | # 49 | # This is also used if you do content translation via gettext catalogs. 50 | # Usually you set "language" from the command line for these cases. 51 | language = None 52 | 53 | # List of patterns, relative to source directory, that match files and 54 | # directories to ignore when looking for source files. 55 | # This pattern also affects html_static_path and html_extra_path. 56 | exclude_patterns = [] 57 | 58 | # The name of the Pygments (syntax highlighting) style to use. 59 | pygments_style = None 60 | 61 | 62 | # -- Options for HTML output ------------------------------------------------- 63 | 64 | # The theme to use for HTML and HTML Help pages. See the documentation for 65 | # a list of builtin themes. 66 | # 67 | html_theme = 'alabaster' 68 | 69 | # Theme options are theme-specific and customize the look and feel of a theme 70 | # further. For a list of options available for each theme, see the 71 | # documentation. 72 | # 73 | # html_theme_options = {} 74 | 75 | # Add any paths that contain custom static files (such as style sheets) here, 76 | # relative to this directory. They are copied after the builtin static files, 77 | # so a file named "default.css" will overwrite the builtin "default.css". 78 | html_static_path = ['nstatic'] 79 | 80 | # Custom sidebar templates, must be a dictionary that maps document names 81 | # to template names. 82 | # 83 | # The default sidebars (for documents that don't match any pattern) are 84 | # defined by theme itself. Builtin themes are using these templates by 85 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 86 | # 'searchbox.html']``. 87 | # 88 | # html_sidebars = {} 89 | 90 | 91 | # -- Options for HTMLHelp output --------------------------------------------- 92 | 93 | # Output file base name for HTML help builder. 94 | htmlhelp_basename = 'apertium-pythondoc' 95 | 96 | 97 | # -- Options for LaTeX output ------------------------------------------------ 98 | 99 | latex_elements = { 100 | # The paper size ('letterpaper' or 'a4paper'). 101 | # 102 | # 'papersize': 'letterpaper', 103 | 104 | # The font size ('10pt', '11pt' or '12pt'). 105 | # 106 | # 'pointsize': '10pt', 107 | 108 | # Additional stuff for the LaTeX preamble. 109 | # 110 | # 'preamble': '', 111 | 112 | # Latex figure (float) alignment 113 | # 114 | # 'figure_align': 'htbp', 115 | } 116 | 117 | # Grouping the document tree into LaTeX files. List of tuples 118 | # (source start file, target name, title, 119 | # author, documentclass [howto, manual, or own class]). 120 | latex_documents = [ 121 | (master_doc, 'apertium-python.tex', 'apertium-python Documentation', 122 | 'Andi Qu', 'manual'), 123 | ] 124 | 125 | 126 | # -- Options for manual page output ------------------------------------------ 127 | 128 | # One entry per manual page. List of tuples 129 | # (source start file, name, description, authors, manual section). 130 | man_pages = [ 131 | (master_doc, 'apertium-python', 'apertium-python Documentation', 132 | [author], 1) 133 | ] 134 | 135 | 136 | # -- Options for Texinfo output ---------------------------------------------- 137 | 138 | # Grouping the document tree into Texinfo files. List of tuples 139 | # (source start file, target name, title, author, 140 | # dir menu entry, description, category) 141 | texinfo_documents = [ 142 | (master_doc, 'apertium-python', 'apertium-python Documentation', 143 | author, 'apertium-python', 'One line description of project.', 144 | 'Miscellaneous'), 145 | ] 146 | 147 | 148 | # -- Options for Epub output ------------------------------------------------- 149 | 150 | # Bibliographic Dublin Core info. 151 | epub_title = project 152 | 153 | # The unique identifier of the text. This can be a ISBN number 154 | # or the project homepage. 155 | # 156 | # epub_identifier = '' 157 | 158 | # A unique identification for the text. 159 | # 160 | # epub_uid = '' 161 | 162 | # A list of files that should not be packed into the epub file. 163 | epub_exclude_files = ['search.html'] 164 | 165 | 166 | # -- Extension configuration ------------------------------------------------- 167 | 168 | # -- Options for intersphinx extension --------------------------------------- 169 | 170 | # Example configuration for intersphinx: refer to the Python standard library. 171 | intersphinx_mapping = {'https://docs.python.org/': None} 172 | 173 | 174 | # reCommonMark Setup 175 | def setup(app): 176 | app.add_config_value('recommonmark_config', { 177 | 'auto_toc_tree_section': 'Contents', 178 | }, True) 179 | app.add_transform(AutoStructify) 180 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | Apertium + Python 2 | ================= 3 | 4 | Contents 5 | -------- 6 | 7 | * [About](about.md) 8 | * [Installation](install.md) 9 | * [Usage](usage.md) 10 | 11 | Apertium 12 | -------- 13 | 14 | ```eval_rst 15 | .. automodule:: apertium 16 | :members: 17 | ``` 18 | 19 | Analysis 20 | -------- 21 | 22 | ```eval_rst 23 | .. automodule:: apertium.analysis 24 | :members: 25 | ``` 26 | 27 | Generation 28 | ---------- 29 | 30 | ```eval_rst 31 | .. automodule:: apertium.generation 32 | :members: 33 | ``` 34 | 35 | Tagger 36 | ------ 37 | 38 | ```eval_rst 39 | .. automodule:: apertium.tagger 40 | :members: 41 | ``` 42 | 43 | Translation 44 | ----------- 45 | 46 | ```eval_rst 47 | .. automodule:: apertium.translation 48 | :members: 49 | ``` 50 | 51 | Indices and tables 52 | ------------------ 53 | 54 | ```eval_rst 55 | * :ref:`genindex` 56 | * :ref:`modindex` 57 | * :ref:`search` 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/source/install.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | From PyPI 5 | ----------- 6 | 7 | - Install from PyPI by running 8 | ``` 9 | pip install apertium 10 | ``` 11 | 12 | - For developers, `pipenv` can be used to install the development dependencies and enter a shell with them: 13 | ``` 14 | pip install pipenv 15 | pipenv install --dev 16 | pipenv shell 17 | ``` 18 | 19 | Apertium packages can be installed from python interpreter as well 20 | - Install `apertium-all-dev` 21 | ```python 22 | import apertium 23 | apertium.installer.install_apertium() 24 | ``` 25 | 26 | - Install language packages 27 | ```python 28 | import apertium 29 | apertium.installer.install_module('eng') 30 | apertium.installer.install_module('en-es') 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/source/usage.md: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | - For multiple invocations `Method 1` is more performant, as the dictionary needs to be loaded only once. 5 | 6 | Analysis 7 | -------- 8 | 9 | Performing Morphological Analysis 10 | 11 | - Method 1: Create an `Analyzer` object and call its `analyze` method. 12 | 13 | ```python 14 | In [1]: import apertium 15 | In [2]: a = apertium.Analyzer('en') 16 | In [3]: a.analyze('cats') 17 | Out[3]: [cats/cat, ./.] 18 | ``` 19 | 20 | - Method 2: Calling `analyze()` directly. 21 | 22 | ```python 23 | In [1]: import apertium 24 | In [2]: apertium.analyze('en', 'cats') 25 | Out[2]: cats/cat 26 | ``` 27 | 28 | Generation 29 | ---------- 30 | 31 | Performing Morphological Generation 32 | 33 | - Method 1: Create a `Generator` object and call its `generate` method. 34 | 35 | ```python 36 | In [1]: import apertium 37 | In [2]: g = apertium.Generator('en') 38 | In [3]: g.generate('-cat$') 39 | Out[3]: 'cats' 40 | ``` 41 | 42 | - Method 2: Calling `generate()` directly. 43 | 44 | ```python 45 | In [1]: import apertium 46 | In [2]: apertium.generate('en', '-cat$') 47 | Out[2]: 'cats' 48 | ``` 49 | 50 | Installing more modes from other language data 51 | ---------------------------------------------- 52 | 53 | One can also install modes by providing the path to the lang-data using this simple function:: 54 | 55 | In [1]: import apertium 56 | In [2]: apertium.append_pair_path('..') 57 | 58 | Install language packages with `apertium.installer.install_module(language_name)`. For example `apertium-eng` can be installed by executing `apertium.installer.install_module('eng')` 59 | 60 | Tagger 61 | ------ 62 | 63 | Performing Tagging:: 64 | 65 | - Method 1: Create a `Tagger` object and call its `tag` method. 66 | 67 | ```python 68 | In [1]: import apertium 69 | In [2]: tagger = apertium.Tagger('eng') 70 | In [3]: tagger.tag('cats') 71 | Out[3]: [cats/cat] 72 | ``` 73 | 74 | - Method 2: Calling `tag()` directly. 75 | 76 | ```python 77 | In [1]: import apertium 78 | In [2]: apertium.tag('en', 'cats') 79 | Out[2]: [cats/cat] 80 | ``` 81 | 82 | Translation 83 | ----------- 84 | 85 | Performing Translations:: 86 | 87 | - Method 1: Create a `Translator` object and call its `translate` method. 88 | 89 | ```python 90 | In [1]: import apertium 91 | In [2]: t = apertium.Translator('eng', 'spa') 92 | In [3]: t.translate('cats') 93 | Out[3]: 'Gatos' 94 | ``` 95 | 96 | - Method 2: Calling `translate()` directly. 97 | 98 | ```python 99 | In [1]: import apertium 100 | In [2]: apertium.translate('en', 'spa', 'cats') 101 | Out[2]: 'Gatos' 102 | ``` 103 | 104 | Installing more modes from other language data 105 | ---------------------------------------------- 106 | 107 | - One can also install modes by providing the path to the lang-data: 108 | 109 | In [1]: import apertium 110 | In [2]: apertium.append_pair_path('..') 111 | 112 | - Install language packages with `apertium.installer.install_module(language_name)`. For example `apertium-eng` can be installed by executing `apertium.installer.install_module('eng')` 113 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from os import path 4 | import platform 5 | import re 6 | from typing import List 7 | 8 | from setuptools import find_packages, setup 9 | from setuptools.command.develop import develop 10 | from setuptools.command.install import install 11 | 12 | 13 | def install_binaries() -> None: 14 | import apertium 15 | 16 | apertium.installer.nightly = True 17 | apertium.installer.install_apertium() 18 | apertium.installer.install_module('eng') 19 | apertium.installer.install_module('eng-spa') 20 | 21 | def kaz_tat_install(): 22 | apertium.installer.nightly = False 23 | ubuntu = apertium.installer.Debian() 24 | if platform.system() == 'Linux': 25 | ubuntu._install_package_source() 26 | apertium.installer.install_module('kaz-tat') 27 | apertium.installer.nightly = True 28 | if platform.system() == 'Linux': 29 | ubuntu._install_package_source() 30 | kaz_tat_install() 31 | 32 | apertium.installer.install_wrapper('python3-apertium-core') 33 | apertium.installer.install_wrapper('python3-apertium-lex-tools') 34 | apertium.installer.install_wrapper('python3-cg3') 35 | apertium.installer.install_wrapper('python3-lttoolbox') 36 | apertium.installer.install_apertium_linux() 37 | 38 | 39 | class CustomInstallCommand(install): 40 | def run(self) -> None: 41 | install.run(self) 42 | install_binaries() 43 | 44 | 45 | class CustomDevelopCommand(develop): 46 | def run(self) -> None: 47 | develop.run(self) 48 | install_binaries() 49 | 50 | 51 | def find_details(find_value: str, file_paths: List[str]) -> str: 52 | pwd = path.abspath(path.dirname(__file__)) 53 | with open(path.join(pwd, *file_paths), 'r') as input_file: 54 | match = re.search(r"^__{}__ = ['\"]([^'\"]*)['\"]".format(find_value), input_file.read(), re.M) 55 | if match: 56 | return match.group(1) 57 | raise RuntimeError('Unable to find {} string.'.format(find_value)) 58 | 59 | 60 | setup( 61 | name='apertium', 62 | author=find_details('author', ['apertium', '__init__.py']), 63 | author_email='sushain@skc.name', 64 | license=find_details('license', ['apertium', '__init__.py']), 65 | version=find_details('version', ['apertium', '__init__.py']), 66 | keywords='apertium machine translation linguistics', 67 | description='Apertium core modules available in Python', 68 | classifiers=[ 69 | 'Development Status :: 3 - Alpha', 70 | 'Topic :: Text Processing :: Linguistic', 71 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 72 | 'Intended Audience :: Science/Research', 73 | 'Intended Audience :: Developers', 74 | 'Programming Language :: Python :: 3.6', 75 | 'Programming Language :: Python :: 3.7', 76 | 'Programming Language :: Python :: 3.8', 77 | 'Programming Language :: Python :: 3 :: Only', 78 | 'Operating System :: Microsoft :: Windows', 79 | 'Operating System :: POSIX', 80 | ], 81 | long_description=open(path.join(path.abspath(path.dirname(__file__)), 'README.md')).read(), 82 | long_description_content_type='text/markdown; charset=UTF-8', 83 | platforms=['Debian', 'Windows'], 84 | url='https://github.com/apertium/apertium-python', 85 | python_requires='>=3.5', 86 | setup_requires=[ 87 | 'apertium-streamparser==5.0.2', 88 | ], 89 | install_requires=[ 90 | 'apertium-streamparser==5.0.2', 91 | ], 92 | test_suite='tests', 93 | package_data={'apertium': ['py.typed']}, 94 | packages=find_packages(exclude=['tests']), 95 | cmdclass={ 96 | 'develop': CustomDevelopCommand, 97 | 'install': CustomInstallCommand, 98 | }, 99 | ) 100 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import os 3 | import platform 4 | import shutil 5 | import sys 6 | from typing import Dict, Tuple 7 | import unittest 8 | 9 | from streamparser import known, SReading 10 | 11 | base_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') 12 | sys.path.append(base_path) 13 | 14 | import apertium # noqa: E402 15 | 16 | 17 | class TestApertiumInit(unittest.TestCase): 18 | def test_append_pair_path(self): 19 | apertium.pair_paths = [] 20 | apertium.analyzers = {} # type: Dict[str, Tuple[str, str]] 21 | apertium.generators = {} # type: Dict[str, Tuple[str, str]] 22 | apertium.taggers = {} # type: Dict[str, Tuple[str, str]] 23 | apertium.pairs = {} # type: Dict[str, str] 24 | apertium.append_pair_path('/usr/share/apertium') 25 | apertium.append_pair_path('/usr/local/share/apertium') 26 | if platform.system() == 'Windows': 27 | apertium.windows_update_path() 28 | if not apertium.pair_paths or not apertium.analyzers or not apertium.generators or not apertium.taggers or not apertium.pairs: 29 | self.fail('Pair Paths not added to the list/dictionary') 30 | 31 | def test_windows_update_path(self): 32 | if platform.system() != 'Windows': 33 | with self.assertRaises(apertium.InstallationNotSupported): 34 | apertium.windows_update_path() 35 | else: 36 | apertium.pair_paths = [] 37 | apertium.analyzers = {} # type: Dict[str, Tuple[str, str]] 38 | apertium.generators = {} # type: Dict[str, Tuple[str, str]] 39 | apertium.taggers = {} # type: Dict[str, Tuple[str, str]] 40 | apertium.pairs = {} # type: Dict[str, str] 41 | apertium.windows_update_path() 42 | if not apertium.pair_paths or not apertium.analyzers or not apertium.generators or not apertium.taggers or not apertium.pairs: 43 | self.fail('Pair Paths not added to the list/dictionary') 44 | 45 | def test_windows_update_path_via_installer(self): 46 | if platform.system() != 'Windows': 47 | with self.assertRaises(apertium.InstallationNotSupported): 48 | apertium.windows_update_path() 49 | else: 50 | apertium.windows_update_path() 51 | test_binaries = TestInstallation() 52 | test_binaries.test_apertium_installer() 53 | 54 | 55 | class TestAnalyze(unittest.TestCase): 56 | def test_analyzer_en(self): 57 | analyzer = apertium.Analyzer('en') 58 | lexical_units = analyzer.analyze('cats') 59 | lexical_unit = lexical_units[0] 60 | self.assertListEqual(lexical_unit.readings, [[SReading(baseform='cat', tags=['n', 'pl'])]]) 61 | self.assertEqual(lexical_unit.wordform, 'cats') 62 | self.assertEqual(lexical_unit.knownness, known) 63 | 64 | def test_analyze_en(self): 65 | lexical_units = apertium.analyze('eng', 'cats') 66 | lexical_unit = lexical_units[0] 67 | self.assertListEqual(lexical_unit.readings, [[SReading(baseform='cat', tags=['n', 'pl'])]]) 68 | self.assertEqual(lexical_unit.wordform, 'cats') 69 | self.assertEqual(lexical_unit.knownness, known) 70 | 71 | def test_uninstalled_mode(self): 72 | with self.assertRaises(apertium.ModeNotInstalled): 73 | apertium.Analyzer('spa') 74 | 75 | 76 | class TestGenerate(unittest.TestCase): 77 | def test_generator_single(self): 78 | generator = apertium.Generator('en') 79 | wordform = generator.generate('^cat$') 80 | self.assertEqual(wordform, 'cats') 81 | 82 | def test_generator_multiple(self): 83 | generator = apertium.Generator('en') 84 | lexical_units = generator.generate('^cat$ ^cat$') 85 | self.assertEqual(lexical_units, 'cats cats') 86 | 87 | def test_generator_bare(self): 88 | generator = apertium.Generator('en') 89 | lexical_units = generator.generate('cat') 90 | self.assertEqual(lexical_units, 'cat') 91 | 92 | def test_generator_uninstalled_mode(self): 93 | generator = apertium.Generator('spa') 94 | with self.assertRaises(apertium.ModeNotInstalled): 95 | generator.generate('cat') 96 | 97 | def test_single(self): 98 | wordform = apertium.generate('en', '^cat$') 99 | self.assertEqual(wordform, 'cats') 100 | 101 | def test_multiple(self): 102 | lexical_units = apertium.generate('en', '^cat$ ^cat$') 103 | self.assertEqual(lexical_units, 'cats cats') 104 | 105 | def test_bare(self): 106 | lexical_units = apertium.generate('en', 'cat') 107 | self.assertEqual(lexical_units, 'cat') 108 | 109 | def test_uninstalled_mode(self): 110 | with self.assertRaises(apertium.ModeNotInstalled): 111 | apertium.generate('spa', 'cat') 112 | 113 | 114 | class TestInstallation(unittest.TestCase): 115 | def test_apertium_installer(self): 116 | # This test doesn't remove existing apertium binaries. 117 | # So it is possible that apertium.installer.install_apertium() isn't working 118 | apertium.installer.install_apertium() 119 | apertium_processes = ['apertium-destxt', 'apertium-interchunk', 'apertium-postchunk', 120 | 'apertium-pretransfer', 'apertium-tagger', 'apertium-transfer', 121 | 'lrx-proc', 'lt-proc' 122 | ] 123 | for process in apertium_processes: 124 | self.assertIsNotNone(shutil.which(process), 'apertium installer not working. {} not available on system path'.format(process)) 125 | break 126 | 127 | @unittest.skipIf(platform.system() == 'Windows', 'apertium binaries not available for windows') 128 | def test_install_apertium_linux(self): 129 | apertium.installer.install_apertium_linux() 130 | apertium_processes = ['apertium-anaphora', 131 | ] 132 | for process in apertium_processes: 133 | self.assertIsNotNone(shutil.which(process), 'apertium linux installer not working. {} not available on system path'.format(process)) 134 | break 135 | 136 | def test_install_module(self): 137 | language = 'kir' 138 | apertium.installer.install_module(language) 139 | self.assertIn(language, apertium.analyzers, 'apetium.install_module not working') 140 | 141 | @unittest.skipIf(platform.system() == 'Windows', 'wrappers not available for windows') 142 | def test_install_wrapper(self): 143 | apertium.installer.install_wrapper('python3-lttoolbox') 144 | if platform.system() == 'Linux': 145 | sys.path.append('/usr/lib/python3/dist-packages') 146 | self.assertIsNotNone(importlib.util.find_spec('lttoolbox'), 'Wrapper not installed') 147 | 148 | 149 | class TestSubProcess(unittest.TestCase): 150 | def setUpModule(): 151 | self._wrappers_available = apertium.utils.wrappers_available 152 | apertium.utils.wrappers_available = False 153 | 154 | def tearDownModule(): 155 | apertium.utils.wrappers_available = self._wrappers_available 156 | 157 | def test_analyze_en_subprocess(self): 158 | test_analyze = TestAnalyze() 159 | test_analyze.test_analyzer_en() 160 | test_analyze.test_analyze_en() 161 | 162 | def test_generate_en_subprocess(self): 163 | test_generate = TestGenerate() 164 | test_generate.test_generator_single() 165 | test_generate.test_generator_multiple() 166 | test_generate.test_generator_bare() 167 | test_generate.test_single() 168 | test_generate.test_multiple() 169 | test_generate.test_bare() 170 | 171 | def test_translate_en_es_subprocess(self): 172 | test_translate = TestTranslate() 173 | test_translate.test_translator_en_spa() 174 | test_translate.test_en_spa() 175 | 176 | def test_tagger_en_subprocess(self): 177 | test_tagger = TestTagger() 178 | test_tagger.test_tagger_en() 179 | test_tagger.test_tag_en() 180 | 181 | 182 | class TestTranslate(unittest.TestCase): 183 | def test_translator_en_spa(self): 184 | translator = apertium.Translator('eng', 'spa') 185 | translated = translator.translate('cats') 186 | self.assertEqual(translated, 'Gatos') 187 | 188 | def test_en_spa(self): 189 | translated = apertium.translate('eng', 'spa', 'cats') 190 | self.assertEqual(translated, 'Gatos') 191 | 192 | def test_en_spa_formatting(self): 193 | translated = apertium.translate('eng', 'spa', 'cats', formatting='txt') 194 | self.assertEqual(translated, 'Gatos') 195 | 196 | def test_kaz_tat(self): 197 | translated = apertium.translate('kaz', 'tat', 'мысық') 198 | self.assertEqual(translated, 'мәче') 199 | 200 | def test_kaz_tat_formatting(self): 201 | translated = apertium.translate('kaz', 'tat', 'мысық', formatting='txt') 202 | self.assertEqual(translated, 'мәче') 203 | 204 | def test_translator_kaz_tat(self): 205 | translator = apertium.Translator('kaz', 'tat') 206 | translated = translator.translate('мысық') 207 | self.assertEqual(translated, 'мәче') 208 | 209 | 210 | class TestTagger(unittest.TestCase): 211 | def test_tagger_en(self): 212 | tagger = apertium.Tagger('en') 213 | lexical_units = tagger.tag('cats') 214 | lexical_unit = lexical_units[0] 215 | self.assertListEqual(lexical_unit.readings, [[SReading(baseform='cat', tags=['n', 'pl'])]]) 216 | self.assertEqual(lexical_unit.wordform, 'cats') 217 | self.assertEqual(lexical_unit.knownness, known) 218 | 219 | def test_tag_en(self): 220 | lexical_units = apertium.tag('eng', 'cats') 221 | lexical_unit = lexical_units[0] 222 | self.assertListEqual(lexical_unit.readings, [[SReading(baseform='cat', tags=['n', 'pl'])]]) 223 | self.assertEqual(lexical_unit.wordform, 'cats') 224 | self.assertEqual(lexical_unit.knownness, known) 225 | 226 | def test_uninstalled_mode(self): 227 | with self.assertRaises(apertium.ModeNotInstalled): 228 | apertium.Tagger('spa') 229 | --------------------------------------------------------------------------------