Repository: PyAr/fades Branch: master Commit: a281c68b6b7e Files: 88 Total size: 452.7 KB Directory structure: gitextract_2mmkzg90/ ├── .flake8 ├── .github/ │ └── workflows/ │ ├── integtests.yaml │ └── tests.yaml ├── .gitignore ├── .readthedocs.yaml ├── AUTHORS ├── COPYING ├── HOWTO_RELEASE.txt ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bin/ │ ├── fades │ └── fades.cmd ├── build_readme ├── docs/ │ ├── Makefile │ ├── conf.py │ ├── index.rst │ ├── pydepmanag.rst │ ├── readme.rst │ └── requirements.txt ├── fades/ │ ├── __init__.py │ ├── __main__.py │ ├── _version.py │ ├── cache.py │ ├── envbuilder.py │ ├── file_options.py │ ├── helpers.py │ ├── logger.py │ ├── main.py │ ├── multiplatform.py │ ├── parsing.py │ ├── pipmanager.py │ └── pkgnamesdb.py ├── man/ │ └── fades.1 ├── pkg/ │ ├── debian/ │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ ├── copyright │ │ ├── rules │ │ └── watch │ └── snap/ │ └── snapcraft.yaml ├── press.txt ├── requirements.txt ├── resources/ │ ├── gifs/ │ │ └── gifs.rst │ ├── notes.txt │ ├── slides.odp │ ├── slides_LT.odp │ └── video/ │ └── script.ods ├── setup.py ├── test ├── testdev ├── testdev.bat └── tests/ ├── __init__.py ├── conftest.py ├── examples/ │ ├── pypi_get_version_fail.json │ └── pypi_get_version_ok.json ├── integtest.py ├── test_cache/ │ ├── __init__.py │ ├── conftest.py │ ├── test_caches.py │ ├── test_comparisons.py │ ├── test_remove.py │ ├── test_selection.py │ └── test_store.py ├── test_envbuilder.py ├── test_file_options.py ├── test_files/ │ ├── fades_as_part_of_other_word.py │ ├── no_req.py │ ├── req_all.py │ ├── req_class.py │ ├── req_def.py │ ├── req_mixed_backends.py │ ├── req_module.py │ ├── req_module_2.py │ └── req_module_3.py ├── test_helpers.py ├── test_infra.py ├── test_logger.py ├── test_main.py ├── test_multiplatform.py ├── test_parsing/ │ ├── test_docstrings.py │ ├── test_file.py │ ├── test_file_reqs.py │ ├── test_manual.py │ ├── test_reqs.py │ └── test_vcs_dependency.py ├── test_pipmanager.py └── test_pkgnamesdb.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .flake8 ================================================ [flake8] max-line-length=99 exclude=.git select=E,W,F,C,N ignore= ================================================ FILE: .github/workflows/integtests.yaml ================================================ name: Integration Tests on: push: branches: [ master ] pull_request: branches: [ master ] jobs: archlinux: runs-on: ubuntu-latest container: # Use https://github.com/gilgamezh/archlinux-python39 to save the python build time image: gilgamezh/archlinux-python39:latest volumes: - ${{ github.workspace }}:/fades steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install dependencies run: | pacman -Suy --noconfirm python3 python-packaging - name: Simple fades run run: | cd /fades bin/fades -v -d pytest -x pytest --version - name: Using a different Python run: | python bin/fades -v --python=python3.9 -d pytest -x pytest -v --integtest-pyversion=3.9 tests/integtest.py fedora: runs-on: ubuntu-latest container: image: fedora:latest volumes: - ${{ github.workspace }}:/fades steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install dependencies run: | yum install --assumeyes python3.13 python-packaging - name: Simple fades run run: | cd /fades bin/fades -v -d pytest -x pytest --version - name: Using a different Python run: | yum install --assumeyes python3.9 cd /fades python3.13 bin/fades -v --python=python3.9 -d pytest -x pytest -v --integtest-pyversion=3.9 tests/integtest.py native-windows: strategy: matrix: # just a selection otherwise it's too much # - latest OS (left here even if it's only one to simplify upgrading later) # - oldest and newest Python os: [windows-2025] python-version: [3.8, "3.13"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v5 id: matrixpy with: python-version: ${{ matrix.python-version }} - name: Also set up Python 3.10 for cross-Python test uses: actions/setup-python@v5 id: otherpy with: python-version: "3.10" - name: Install dependencies run: | ${{ steps.matrixpy.outputs.python-path }} -m pip install -U packaging - name: Simple fades run run: | ${{ steps.matrixpy.outputs.python-path }} bin/fades -v -d pytest -x pytest --version - name: Using a different Python run: | ${{ steps.matrixpy.outputs.python-path }} bin/fades -v --python=${{ steps.otherpy.outputs.python-path }} -d pytest -x pytest -v --integtest-pyversion=3.10 tests/integtest.py native-generic: strategy: matrix: # just a selection otherwise it's too much # - latest OSes # - oldest and newest Python os: [ubuntu-24.04, macos-15] python-version: [3.8, "3.13"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v5 id: matrixpy with: python-version: ${{ matrix.python-version }} - name: Also set up Python 3.10 for cross-Python test uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install dependencies run: | ${{ steps.matrixpy.outputs.python-path }} -m pip install -U packaging - name: Simple fades run run: | ${{ steps.matrixpy.outputs.python-path }} bin/fades -v -d pytest -x pytest --version - name: Using a different Python run: | ${{ steps.matrixpy.outputs.python-path }} bin/fades -v --python=python3.10 -d pytest -x pytest -v --integtest-pyversion=3.10 tests/integtest.py ================================================ FILE: .github/workflows/tests.yaml ================================================ name: Tests on: push: branches: [ master ] pull_request: branches: [ master ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: run-tests: strategy: matrix: os: [ubuntu-22.04, ubuntu-24.04, macos-14, macos-15, windows-2022, windows-2025] python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | pip install -U -r requirements.txt - name: Run tests run: | pytest ================================================ FILE: .gitignore ================================================ # Generated as a side effect of development README.html # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg .mypy_cache/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # archlinux !pkg/archlinux/PKGBUILD pkg/archlinux/* # Ides .vscode/ .idea/ ================================================ FILE: .readthedocs.yaml ================================================ # Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details --- version: 2 build: os: ubuntu-22.04 tools: python: "3.12" sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt ================================================ FILE: AUTHORS ================================================ Alejandro Dau Andrés Delfino Ariel Rossanigo Berenice Larsen Pereyra David Litvak Bruno Diego Duncan Diego Mascialino Eduardo Enriquez Facundo Batista FaQ Filipe Ximenes gera jairot Javier Andres Mansilla Juan Juan Carlos Lucio Torre Manuel Kaufmann Martin Alderete matuu Nicolás Demarchi Ricardo Kirkner Rushil Patel ================================================ FILE: COPYING ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: HOWTO_RELEASE.txt ================================================ Steps before a release is done ------------------------------ Check all is crispy rm -rf build dist ./setup.py clean build ./setup.py clean sdist Edit the ``fades/_version.py`` file properly, then tag and commit/push git tag release-VERSION git commit -am "Release VERSION" git push --tag How to release it to PyPI ------------------------- Dead simple: rm -rf build dist ./setup.py clean sdist fades -d twine -x twine upload dist/fades-* How to create a .deb -------------------- Create the tarball: rm -rf build dist ./setup.py clean sdist Copy this tarball to a clean dir, renaming as "orig" mkdir /tmp/fades_pack cp dist/fades-X.Y.tar.gz /tmp/fades_pack/fades_X.Y.orig.tar.gz cd /tmp/fades_pack Most of next instructions come from http://wiki.debian.org/Python/GitPackaging tar -xf fades_X.Y.orig.tar.gz cd fades-X.Y git init git add . git commit -m "import fades_X.Y.orig.tar.gz" git checkout -b upstream pristine-tar commit ../fades_X.Y.orig.tar.gz upstream git-dpm init ../fades_X.Y.orig.tar.gz Copy the project's debian dir and change changelog (be sure that this "debian" dir is properly updated in the project... notably, be sure copyright year is current one and also that no new dependencies were introduced since last release). cp -pr $DEVEL/fades/pkg/debian . dch # doing the following: - version should be (X.Y-1) unstable - just leave one "* Initial release." Continue with preparations: git add debian/* git commit -m "Added debian dir." git-dpm prepare git-dpm status Build the .deb debuild -us -uc -I -i To test the .deb you just created: sudo dpkg -i *.deb If you want to uninstall it do: sudo dpkg -r fades How to release it to Debian --------------------------- Need just to report a bug very similar to this one: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=814913 For that, just run the following and answer the questions: reportbug -B debian (I had to do that twice, not sure why: first time it asked some questions and then errored out, second time it picked up from there and finish the job) How to release it to Arch ------------------------- Changes should be applied in the AUR repo ``ssh://aur@aur.archlinux.org/fades.git`` Edit ``pkg/archlinux/PKGBUILD`` and set *pkgver* and *sha256sums*, then run ``makepkg --printsrcinfo > .SRCINFO`` to update the .SRCINFO file. Finally commit and push the changes. The package will be automatically updated in AUR. ``https://aur.archlinux.org/packages/fades`` How to release it to the Snap store ----------------------------------- Edit the snapcraft yaml and change the "version" number vi pkg/snap/snapcraft.yaml Build the snap (no need to specify the YAML, there is a hidden symlink to it): snapcraft Push it to the store and release: snapcraft push fades_6.0_amd64.snap snapcraft release fades edge beta Test it: sudo snap install fades --channel=beta --classic /snap/fades/current/bin/fades -V If all is fine, release it to candidate: snapcraft release fades candidate Announce internally: telegram, fades mail list. Wait 3 days or so, and release to stable: snapcraft release fades stable Announce in the snapcraft forum, something similar to: https://forum.snapcraft.io/t/call-for-testing-fades-7/5070 Read the Docs ------------- - go and login there, go to "fades" project - in "Versions" tab, activate the new version (corresponding to the release in github) - in "Administrator" tab, go to option "Advanced configuration" at the left, choose latest release as "default version" - verify all latest is seen in http://fades.rtfd.org/ How to sign the files --------------------- If you are putting files to download (notably, installators: .deb, tarballs, etc) it's a good idea to sign them and offer checksums, in case of somebody wanting to validate the files. To sign it: gpg --armor --sign --detach-sig FILENAME To create the checksum: sha1sum FILENAME > FILENAME.sha1 Final steps ----------- - Remember to update the .deb and .tar.gz in www.taniquetil.com.ar/fades - Create a change log and send press releases: - a tweet, - PyAr mail list, IRC and Telegram group - fades' Telegram group and mail list - python-announces ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: MANIFEST.in ================================================ include README.rst include COPYING include AUTHORS include man/fades.1 ================================================ FILE: README.rst ================================================ What is fades? ============== .. image:: https://github.com/PyAr/fades/actions/workflows/test.uaml/badge.svg :target: https://github.com/PyAr/fades/actions/workflows/test.uaml/badge.svg .. image:: https://readthedocs.org/projects/fades/badge/?version=latest :target: http://fades.readthedocs.org/en/latest/?badge=latest :alt: Documentation Status .. image:: https://badge.fury.io/py/fades.svg :target: https://badge.fury.io/py/fades .. image:: https://coveralls.io/repos/PyAr/fades/badge.svg?branch=master&service=github :target: https://coveralls.io/github/PyAr/fades?branch=master .. image:: https://build.snapcraft.io/badge/PyAr/fades.svg :target: https://build.snapcraft.io/user/PyAr/fades :alt: Snap Status .. image:: https://ci.appveyor.com/api/projects/status/crkqv82t1l731fms/branch/master?svg=true :target: https://ci.appveyor.com/project/facundobatista/fades :alt: Appveyor Status fades is a system that automatically handles the virtual environments in the cases normally found when writing scripts and simple programs, and even helps to administer big projects. .. image:: resources/logo256.png *fades* will automagically create a new virtual environment (or reuse a previous created one), installing the necessary dependencies, and execute your script inside that virtual environment, with the only requirement of executing the script with *fades* and also marking the required dependencies. *(If you don't have a clue why this is necessary or useful, I'd recommend you to read this small text about* `Python and the Management of Dependencies `_ *.)* The first non-option parameter (if any) would be then the child program to execute, and any other parameters after that are passed as is to that child script. *fades* can also be executed without passing a child script to execute: in this mode it will open a Python interactive interpreter inside the created/reused virtual environment (taking dependencies from ``--dependency`` or ``--requirement`` options). .. contents:: How to use it? ============== Click in the following image to see a video/screencast that shows most of fades features in just 5'... .. image:: resources/video/screenshot.png :target: https://www.youtube.com/watch?v=BCTd_TyCm98 ...or inspect `these several small GIFs `_ that show each a particular `fades` functionality, but please keep also reading for more detailed information... Yes, please, I want to read --------------------------- When you write an script, you have to take two special measures: - need to execute it with *fades* (not *python*) - need to mark those dependencies At the moment you execute the script, fades will search a virtual environment with the marked dependencies, if it doesn't exists fades will create it, and execute the script in that environment. How to execute the script with fades? ------------------------------------- You can always call your script directly with fades:: fades myscript.py However, for you to not forget about fades and to not execute it directly with python, it's better if you put at the beggining of the script the indication for the operating system that it should be executed with fades... :: #!/usr/bin/env fades ...and also set the executable bit in the script:: chmod +x yourscript.py You can also execute scripts directly from the web, passing directly the URL of the pastebin where the script is pasted (most common pastebines are supported, pastebin.com, gist, linkode.org, but also it's supported if the URL points to the script directly):: fades http://myserver.com/myscript.py How to mark the dependencies to be installed? --------------------------------------------- The procedure to mark a module imported by the script as a *dependency to be installed by fades* is by using a comment. This comment will normally be in the same line of the import (recommended, less confusing and less error prone in the future), but it also can be in the previous one. The simplest comment is like:: import somemodule # fades from somepackage import othermodule # fades The ``fades`` is mandatory, in this examples the repository is PyPI, see `About different repositories`_ below for other examples. With that comment, *fades* will install automatically in the virtual environment the ``somemodule`` or ``somepackage`` from PyPI. Also, you can indicate a particular version condition, examples:: import somemodule # fades == 3 import somemodule # fades >= 2.1 import somemodule # fades >=2.1,<2.8,!=2.6.5 Sometimes, the project itself doesn't match the name of the module; in these cases you can specify the project name (optionally, before the version):: import bs4 # fades beautifulsoup4 import bs4 # fades beautifulsoup4 == 4.2 What if no script is given to execute? -------------------------------------- If no script or program is passed to execute, *fades* will provide a virtual environment with all the indicated dependencies, and then open an interactive interpreter in the context of that virtual environment. Here is where it comes very handy the ``-i/--ipython`` option, if that REPL is preferred over the standard one. In the case of using an interactive interpreter, it's also very useful to make *fades* to automatically import all the indicated dependencies, passing the ``--autoimport`` parameter. Other ways to specify dependencies ---------------------------------- Apart of marking the imports in the source file, there are other ways to tell *fades* which dependencies to install in the virtual environment. One way is through command line, passing the ``--dependency`` parameter. This option can be specified multiple times (once per dependency), and each time the format is ``repository::dependency``. The dependency may have versions specifications, and the repository is optional (defaults to 'pypi'). Another way is to specify the dependencies in a text file, one dependency per line, with each line having the format previously described for the ``--dependency`` parameter. This file is then indicated to fades through the ``--requirement`` parameter. This option can be specified multiple times. In case of multiple definitions of the same dependency, command line overrides everything else, and requirements file overrides what is specified in the source code. Finally, you can include package names in the script docstring, after a line where "fades" is written, until the end of the docstring; for example:: """Script to do stuff. It's a very important script. We need some dependencies to run ok, installed by fades: request otherpackage """ About different repositories ---------------------------- *fades* supports installing the required dependencies from multiples repositories: besides PyPI, you can specify URLs that can point to projects from GitHub, Launchpad, etc. (basically, everything that is supported by ``pip`` itself). When a dependency is specified, *fades* deduces the proper repository. For example, in the following examples *fades* will install requests from the latest revision from PyPI in the first case, and in the second case the latest revision from the project itself from GitHub:: -d requests -d git+https://github.com/kennethreitz/requests.git#egg=requests If you prefer, you can be explicit about which kind of repository *fades* should use, prefixing the dependency with the special token double colon (``::``):: -d pypi::requests -d vcs::git+https://github.com/kennethreitz/requests.git#egg=requests There are two basic repositories: ``pypi`` which will make *fades* to install the desired dependency from PyPI, and ``vcs``, which will make *fades* to treat the dependency as a URL for a version control system site. In the first case, for PyPI, a full range of version comparators can be specified, as usual. For ``vcs`` repositories, though, the comparison is always exact: if the very same dependency is specified, a *virtual environment* is reused, otherwise a new one will be created and populated. In both cases (specifying the repository explicitly or implicitly) there is no difference if the dependency is specified in the command line, in a ``requirements.txt`` file, in the script's docstring, etc. In the case of marking the ``import`` directly in the script, it slightly different. When marking the ``import`` it normally happens that the package itself to be installed has the name of the imported module, and because of that it can only be found in PyPI. So, in the following cases the ``pypi`` repository is not only deduced, but unavoidable:: import requests # fades from foo import bar # fades import requests # fades <= 3 But if the package is specified (normally needed because it's different than the module name), or if a version control system URL is specified, the same possibilities stated above are available: let *fades* to deduce the proper repository or mark it explicitly:: import bs4 # fades beautifulsoup import bs4 # fades pypi::beautifulsoup import requests # fades git+https://github.com/kennethreitz/requests.git#egg=requests import requests # fades vcs::git+https://github.com/kennethreitz/requests.git#egg=requests One last detail about the ``vcs`` repository: the format to write the URLs is the same (as it's passed without modifications) than what ``pip`` itself supports (see `pip docs `_ for more details). Furthermore, you can install from local projects. It's just fine to use a dependency that starts with ``file:``. E.g. (please note the triple slash, because we're mixing the protocol indication with the path):: fades -d file:///home/crazyuser/myproject/allstars/ How to control the virtual environment creation and usage? ---------------------------------------------------------- You can influence several details of all the virtual environment related process. The most important detail is which version of Python will be used in the virtual environment. Of course, the corresponding version of Python needs to be installed in your system, but you can control exactly which one to use. No matter which way you're executing the script (see above), you can pass a ``-p`` or ``--python`` argument, indicating the Python version to be used just with the number (``3.9``), the whole name (``python3.9``) or the whole path (``/usr/bin/python3.9``). Other detail is the verbosity of *fades* when telling what is doing. By default, *fades* only will use stderr to tell if a virtual environment is being created, and to let the user know that is doing an operation that requires an active network connection (e.g. installing a new dependency). If you call *fades* with ``-v`` or ``--verbose``, it will send all internal debugging lines to stderr, which may be very useful if any problem arises. On the other hand if you pass the ``-q`` or ``--quiet`` parameter, *fades* will not show anything (unless it has a real problem), so the original script stderr is not polluted at all. If you want to use IPython shell you need to call *fades* with ``-i`` or ``--ipython`` option. This option will add IPython as a dependency to *fades* and it will launch this shell instead of the python one. You can also use ``--system-site-packages`` to create a venv with access to the system libs. Finally, no matter how the virtual environment was created, you can always get the base directory of the virtual environment in your system using the ``--where`` (or its alias ``--get-venv-dir``) option. Running programs in the context of the virtual environment ---------------------------------------------------------- The ``-x/--exec`` parameter allows you to execute any program (not just a Python one) in the context of the virtual environment. By default the mandatory given argument is considered the executable name, relative to the environment's ``bin`` directory, so this is specially useful to execute installed scripts/program by the declared dependencies. E.g.:: fades -d flake8 -x flake8 my_script_to_be_verified_by_flake8.py Take in consideration that you can pass an absolute path and it will be respected (but not a relative path, as it will depend of the virtual environment location). For example, if you want to run a shell script that in turn runs a Python program that needs to be executed in the context of the virtual environment, you can do the following:: fades -r requirements.txt --exec /var/lib/foobar/special.sh Finally, if the intended code to run is prepared to be executed as a module (what you would normally run as `python3 -m some_module`), you can use the same parameter with *fades* to run that module inside the virtual environment:: fades -r requirements.txt -m some_module How to deal with packages that are upgraded in PyPI --------------------------------------------------- When you tell *fades* to create a virtual environment using one dependency and don't specify a version, it will install the latest one from PyPI. For example, you do ``fades -d foobar`` and it installs foobar in version 7. At some point, there is a new version of foobar in PyPI, version 8, but if do ``fades -d foobar`` it will just reuse previously created virtual environment, with version 7, not downloading the new version and creating a new virtual environment with it! You can tell fades to do otherwise, just do:: fades -d foobar --check-updates ...and *fades* will search updates for the package on PyPI, and as it will found version 8, will create a new virtual environment using the latest version. You can also use the ``-U`` option as an alias for ``--check-updates``:: fades -d foobar -U From this moment on, if you request ``fades -d foobar`` it will bring the virtual environment with the new version. If you want to get a virtual environment with not-the-latest version for any dependency, just specify the proper versions. You can even use the ``--check-updates`` parameter when specifying the package version. Say you call ``fades -d foobar==7``, *fades* will install version 7 no matter which one is the latest. But if you do:: fades -d foobar==7 --check-updates ...it will still use version 7, but will inform you that a new version is available! What about pinning dependencies? -------------------------------- One nice benefit of *fades* is that every time dependencies change in your project, you actually get to use a new virtual environment automatically. If you don't pin the dependencies in your requirements file, this has another nice side effect: everytime you use them in a new environment (or if you have `--check-updates` set) you will get latest versions, effectively avoiding the trap of sticking in old versions forever. However, this has a bad side. If it happens that a dependency of your project released a revision between the moment you run the tests and the moment your project is deployed to the server, it may happen that you actually put in production an untested combination. Furthermore, it may happen that even if you do pin your dependencies, the dependencies of those dependencies may not be pinned, and you get into the same situation. For example, you may have the ``requests == 2.19.1`` dependency, but ``requests`` declares its own dependencies, for example ``chardet >= 3.0.2``, and when running tests locally you may get ``chardet`` in version ``3.0.3``, but nothing guarantees you that when deploying your project to a server (effectively building everything from scratch) you will not get a newer version of ``chardet``, which may be totally fine but in fact it's something that you did NOT test locally. Here is where *fades* comes to the rescue with the ``--freeze`` option. If this parameter is given, *fades* will operate exactly as it normally would, but also will dump the result of ``pip freeze`` into the specified file. So to continue with the example above, you could run your tests like:: fades -d "requests == 2.19.1" --freeze=reqs-frozen.txt -x python3 -m unittest ...which will leave you ``reqs-frozen.txt`` with a content similar to:: certifi==2018.4.16 chardet==3.0.4 pip==18.0 requests==2.19.1 ... And then you could use *that file* for deployment, which has *all packages* pinned, so you will get exactly what you was expecting. Under the hood options ---------------------- For particular use cases you can send specifics arguments to the ``venv`` module, ``pip`` and ``python`` itself, using the ``--venv-options``, ``--pip-options`` and ``--python-options`` modifiers respectively. You have to use that argument for each argument sent. Examples: ``fades -d requests --venv-options="--symlinks"`` ``fades -d requests --pip-options="--index-url='http://example.com'"`` ``fades --python-options=-B foo.py`` Setting options using config files ---------------------------------- You can also configure fades using `.ini` config files. fades will search config files in `/etc/fades/fades.ini`, the path indicated by `xdg` for your system (for example `~/config/fades/fades.ini`) and `.fades.ini`. So you can have different settings at system, user and project level. With fades installed you can get your config dir running:: python -c "from fades.helpers import get_confdir; print(get_confdir())" The config files are in `.ini` format. (configparser) and fades will search for a `[fades]` section. You have to use the same configurations that in the CLI. The only difference is with the config options with a dash, it has to be replaced with a underscore.:: [fades] ipython=true verbose=true python=python3 check_updates=true dependency=requests;django>=1.8 # separated by semicolon There is a little difference in how fades handle these settings: "dependency", "pip-options" and "venv-options". In these cases you have to use a semicolon separated list. The most important thing is that these options will be merged. So if you configure in `/etc/fades/fades.ini` "dependency=requests" you will have requests in all the virtual environments created by fades. How to clean up old virtual environments? ----------------------------------------- When using *fades* virtual environments are something you should not have to think about. *fades* will do the right thing and create a new virtual environment that matches the required dependencies. There are cases however when you'll want to do some clean up to remove unnecessary virtual environments from disk. By running *fades* with the ``--rm`` argument, *fades* will remove the virtual environment matching the provided UUID if such environment exists (one easy way to find out the environment's UUID is calling *fades* with the ``--where`` option). Another way to clean up the cache is to remove all venvs that haven't been used for some time. In order to do this you need to call *fades* with ``--clean-unused-venvs``. When fades it's called with this option, it runs in mantain mode, this means that fades will exit after finished this task. All virtual environments that haven't been used for more days than the value indicated in param will be removed. It is recommended to have some automatically way of run this option; ie, add a cron task that perform this command:: fades --clean-unused-venvs=42 Some command line examples -------------------------- Execute ``foo.py`` under *fades*, passing the ``--bar`` parameter to the child program, in a virtual environment with the dependencies indicated in the source code:: fades foo.py --bar Execute ``foo.py`` under *fades*, showing all the *fades* messages (verbose mode):: fades -v foo.py Execute ``foo.py`` under *fades* (passing the ``--bar`` parameter to it), in a virtual environment with the dependencies indicated in the source code and also ``dependency1`` and ``dependency2`` (any version > 3.2):: fades -d dependency1 -d "dependency2>3.2" foo.py --bar Execute the Python interactive interpreter in a virtual environment with ``dependency1`` installed:: fades -d dependency1 Execute the Python interactive interpreter in a virtual environment after installing there all dependencies taken from the ``requirements.txt`` file:: fades -r requirements.txt Execute the Python interactive interpreter in a virtual environment after installing there all dependencies taken from files ``requirements.txt`` and ``requirements_devel.txt``:: fades -r requirements.txt -r requirements_devel.txt Use the ``django-admin.py`` script to start a new project named ``foo``, without having to have django previously installed:: fades -d django -x django-admin.py startproject foo Remove a virtual environment matching the given uuid from disk and cache index:: fades --rm 89a2bf83-c280-4918-a78d-c35506efd69d Download the script from the given pastebin and executes it (previously building a virtual environment for the dependencies indicated in that pastebin, of course):: fades http://linkode.org/#4QI4TrPlGf1gK2V7jPBC47 Run all the tests in a project (running ``pytest`` directly as a module, for better behaviour) and at the same time freeze dependencies for later deployment:: fades -r requirements.txt --freeze -m pytest -v Some examples using fades in project scripts -------------------------------------------- Including *fades* in project helper scripts makes it easy to stop worrying about the virtual environment activation/deactivation when working in that project, and also solves the problem of needing to update/change/fix an already created virtual environment if the dependencies change. This is an example of how a script to run your project may look like:: #!/bin/sh if (command -v fades > /dev/null) then # fades FTW! fades -r requirements.txt bin/start else echo 2 # hope you are in the correct virtual environment python3 bin/start fi To run the tests, it's super handy to have a script that also takes care of the development dependencies:: #!/bin/sh fades -r requirements.txt -r reqs-dev.txt -x python -m pytest -s "$@" What if Python is updated in my system? --------------------------------------- The virtual environments created by fades depend on the Python version used to create them, considering its major and minor version. This means that if run fades with a Python version and then run it again with a different Python version, it may need to create a new virtual environment. Let's see some examples. Let's say you run fades with ``python``, which is a symlink in your ``/usr/bin/`` to ``python3.6`` (running it directly by hand or because fades is installed to use that Python version). If you have Python 3.6.2 installed in your system, and it's upgraded to Python 3.6.3, fades will keep reusing the already created virtual environments, as only the micro version changed, not minor or major. But if Python 3.7 is installed in your system, and the default ``python`` is pointed to this new one, fades will start creating all the virtual environments again, with this new version. This is a good thing, because you want that the dependencies installed with one specific Python in the virtual environment are kept being used by the same Python version. However, if you want to avoid this behaviour, be sure to always call fades with the specific Python version (``/usr/bin/python3.6`` or ``python3.6``, for example), so it won't matter if a new version is available in the system. How to install it ================= Several instructions to install ``fades`` in different platforms. Simplest way ------------ In some systems you can install ``fades`` directly, no needing to install previously any dependency. If you are in debian unstable or testing, just do: sudo apt-get install fades For Arch Linux, you can install it from the **AUR** using any `AUR helper `_, e.g. with ``pikaur``: pikaur -S fades In systems with Snaps: snap install fades --classic (why `--classic`? Because it's the only way that `fades` could, from inside the snap, access the rest of the system in case you want to use a different Python version, or a dependency that needs compilation, etc). For Mac OS X (and `Homebrew `_): brew install fades Else, keep reading to know how to install the dependencies first, and ``fades`` in your system next. Dependencies ------------ Besides needing Python 3.6 or greater, fades depends on the ``python-xdg`` package. This package should be installed on any GNU/Linux OS wiht a freedesktop.org GUI. However it is an **optional** dependency. You can install it in Ubuntu/Debian with:: apt-get install python3-xdg And on Arch Linux with:: pacman -S python-xdg For others debian and ubuntu ---------------------------- If you are NOT in debian unstable or testing (if you are, see above for better instructions), you can use this `.deb `_. Download it and install doing:: sudo dpkg -i fades_*.deb Using pip if you want ---------------------- :: pip3 install fades Multiplatform tarball --------------------- Finally you can always get the multiplatform tarball and install it in the old fashion way:: wget http://ftp.debian.org/debian/pool/main/f/fades/fades_9.0.1.orig.tar.gz tar -xf fades_*.tar.gz cd fades-* sudo ./setup.py install Can I try it without installing it? ----------------------------------- Yes! Branch the project and use the executable:: git clone https://github.com/PyAr/fades.git cd fades bin/fades your_script.py What about Windows? ------------------- Windows is a platform supported by fades. However, we don't have a proper Windows installer (a ``.exe`` or ``.msi``), but you can install it using ``pip``, or from the tarball, or try it directly from the project. All these options are properly described above. We *do* want to have a Windows installer. If you can help us in this regard, please contact us. Also we would want a Travis running in Windows so that GitHub runs all the tests in this platform too before landing any code. Thanks! Get some help, give some feedback ================================= You can ask any question or send any recommendation or request to the `mailing list `_. Come chat with us on IRC. The #fades channel is located at the `Freenode `_ network. Also, you can open an issue `here `_ (please do if you find any problem!). Thanks in advance for your time. How to develop fades itself =========================== Quick guide to get you up and running in fades development. Getting the code ---------------- Clone the project:: git clone git@github.com:PyAr/fades.git Install dependencies -------------------- *fades* manages it's own dependencies, so there is nothing extra you need to install. To try it, just do:: bin/fades -V How to run the tests -------------------- When starting development, at all times, and specially before wrapping up a new branch, you need to be sure that all tests pass ok. This is very simple, actually, just run:: ./test That will not only check test cases, but also that the code complies with aesthetic recommendations, and that the README document has a proper format. If you want to run *one* particular test, just specify it. Example:: ./test tests.test_main:DepsMergingTestCase.test_two_different Development process ------------------- Just pick an issue from `the list `_. Develop, assure ``./test`` is happy, commit, push, create a pull request, etc. Please, if you aim for creating a Pull Request with new code (functionality or fixes), include tests for your changes. Thanks! Enjoy. ================================================ FILE: bin/fades ================================================ #!/usr/bin/env python3 # # Copyright 2014 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Script to run the 'fades' utility.""" import os import sys try: import packaging except ImportError: print("Import failed for `packaging` dependency. Please do `pip3 install packaging` and try again") exit(-1) # small hack to allow fades to be run directly from the project, using code # from project itself, not anything already installed in the system parent_dir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0]))) if os.path.basename(parent_dir).startswith('fades'): # inside the project or an opened tarball!! sys.path.insert(0, parent_dir) from fades import main, FadesError # noqa (imports after fixing the path, not at the top) try: rc = main.go() except FadesError: sys.exit(-1) sys.exit(rc) ================================================ FILE: bin/fades.cmd ================================================ :: :: Copyright 2018 Facundo Batista, Nicolás Demarchi :: :: This program is free software: you can redistribute it and/or modify it :: under the terms of the GNU General Public License version 3, as published :: by the Free Software Foundation. :: :: This program is distributed in the hope that it will be useful, but :: WITHOUT ANY WARRANTY; without even the implied warranties of :: MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR :: PURPOSE. See the GNU General Public License for more details. :: :: You should have received a copy of the GNU General Public License along :: with this program. If not, see . :: :: For further info, check https://github.com/PyAr/fades :: Script to run the 'fades' utility in Windows python -m fades %* ================================================ FILE: build_readme ================================================ FADES='./bin/fades -r requirements.txt' python3 setup.py --long-description | $FADES -x rst2html5 > README.html ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/fades.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/fades.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/fades" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/fades" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: docs/conf.py ================================================ # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'fades' copyright = '2024, Facundo Batista, Nicolás Demarchi' author = 'Facundo Batista, Nicolás Demarchi' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ 'sphinx_rtd_theme', ] templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] ================================================ FILE: docs/index.rst ================================================ .. fades documentation master file, created by sphinx-quickstart on Sat Dec 26 19:29:13 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to fades's documentation! ================================= Contents: --------- .. toctree:: :maxdepth: 2 readme development Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs/pydepmanag.rst ================================================ Python and the Management of Dependencies ========================================= Python has an extensive standard library ("batteries included!"), but is frequent the necessity of using other modules not included there, mostly from the Python Package Index (`PyPI `_). The original way of installing those modules is at "system level" (`sudo pip install foobar`), in the operating system in a general way, making them available to be used by any program that is executed. Beyond needing root or administrator level to install the dependencies in that way, the first problem we find are conflicts: the typical case of two programs needing two different versions of the same dependency, which can not be achieved when installing the dependencies globally. This is why is so normal in Python to use "virtual environments" (or "virtualenvs"). A new virtualenv is created for each program, the needed dependencies for each program are installed in the corresponding virtualenv, and as stuff in a virtualenv is only accessible from inside the virtualenv, there are no conflicts anymore. At this point, however, we hit the problem of the administration of the virtualenvs themselves: create them, install modules in them, activate them to be uses by each program and deactivate them later, remember the names of each environment for each program, etc. To automatize this, `fades `_ was born. *fades* allows you to unleash all the power of virtualenvs without needing to worry about them. Do you want to execute a script that needs the ``foobar`` dependency? ``fades -d foobar script.py`` Do you want an interactive interpreter having ``foobar`` installed as dependency? ``fades -d foobar`` Do you need to execute the script but with several dependencies, one with a specific version? ``fades -d foo -d bar -d baz==1.1 script.py`` Do you have all the dependencies in a requirements file? ``fades -r requirements.txt script.py`` These are only simple examples of what you can do with *fades*. Virtual environments are a very powerful tool, and automate and simplify their use makes *fades* to have a lot of options, some that you will use everyday, others that will prove useful in some specific situations. Start to use *fades* step by step (`check the docs `_) and will find that it will solve the dependencies management in your programs and scripts, using virtualenvs but without the complexity of having to deal with them by hand. ================================================ FILE: docs/readme.rst ================================================ .. include:: ../README.rst ================================================ FILE: docs/requirements.txt ================================================ sphinx_rtd_theme ================================================ FILE: fades/__init__.py ================================================ # Copyright 2015-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General # Public License version 3, as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. # If not, see . # # For further info, check https://github.com/PyAr/fades """Main package.""" from ._version import __version__, VERSION # NOQA; provides module level version attr class FadesError(Exception): """Provides a Fades exception.""" REPO_PYPI = 'pypi' REPO_VCS = 'vcs' ================================================ FILE: fades/__main__.py ================================================ """Init file to allow execution of fades as a module.""" import sys from fades import main, FadesError try: rc = main.go() except FadesError: sys.exit(-1) sys.exit(rc) ================================================ FILE: fades/_version.py ================================================ """Holder of the fades version number.""" VERSION = (9, 0, 2) __version__ = '.'.join([str(x) for x in VERSION]) ================================================ FILE: fades/cache.py ================================================ # Copyright 2015-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General # Public License version 3, as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. # If not, see . # # For further info, check https://github.com/PyAr/fades """The cache manager for virtualenvs.""" import json import logging import os import time from fades import REPO_VCS from fades.multiplatform import filelock from fades.parsing import VCSDependency, NameVerDependency logger = logging.getLogger(__name__) class VEnvsCache: """A cache for virtualenvs.""" def __init__(self, filepath): """Init.""" logger.debug("Using cache index: %r", filepath) self.filepath = filepath self.lockpath = filepath + ".lock" def _venv_match(self, installed, requirements): """Return True if what is installed satisfies the requirements. This method has multiple exit-points, but only for None (because if *anything* is not satisified, the venv is no good). Only after all was checked, and it didn't exit, the venv is ok and it so returns the satisfying dependencies. """ if not requirements: # special case for no requirements, where we can't actually # check anything: the venv is useful if nothing installed too return None if installed else [] satisfying_deps = [] for repo, req_deps in requirements.items(): useful_inst = set() if repo not in installed: # the venv doesn't even have the repo return None if repo == REPO_VCS: inst_namevers = {(url, None) for url in installed[repo].keys()} else: inst_namevers = {(dep, ver) for (dep, ver) in installed[repo].items()} for req in req_deps: for inst_name, inst_ver in inst_namevers: if req.name == inst_name and req.specifier.contains(inst_ver): useful_inst.add((inst_name, inst_ver)) break else: # nothing installed satisfied that requirement return None # assure *all* that is installed is useful for the requirements if useful_inst == inst_namevers: inst_reqs = set() for name, ver in inst_namevers: if ver is None: inst_reqs.add(VCSDependency(name)) else: inst_reqs.add(NameVerDependency(name, ver)) satisfying_deps.extend(inst_reqs) else: return None # it did it through! return satisfying_deps def _match_by_uuid(self, current_venvs, uuid): """Select a venv matching exactly by uuid.""" for venv_str in current_venvs: venv = json.loads(venv_str) env_path = venv.get('metadata', {}).get('env_path') _, env_uuid = os.path.split(env_path) if env_uuid == uuid: return venv def _select_better_fit(self, matching_venvs): """Receive a list of matching venvs, and decide which one is the best fit.""" # keep the venvs in a separate array, to pick up the winner, and the (sorted, to compare # each dependency with its equivalent) in other structure to later compare venvs = [] to_compare = [] for matching, venv in matching_venvs: to_compare.append(sorted(matching, key=lambda req: getattr(req, 'key', ''))) venvs.append(venv) # compare each n-tuple of dependencies to see which one is bigger, and add score to the # position of the winner scores = [0] * len(venvs) for dependencies in zip(*to_compare): if not isinstance(dependencies[0], NameVerDependency): # only distribution URLs can be compared continue winner = dependencies.index(max(dependencies)) scores[winner] = scores[winner] + 1 # get the rightmost winner (in case of ties, to select the latest venv) winner_pos = None winner_score = -1 for i, score in enumerate(scores): if score >= winner_score: winner_score = score winner_pos = i return venvs[winner_pos] def _match_by_requirements(self, current_venvs, requirements, interpreter, options): """Select a venv matching interpreter and options, complying with requirements. Several venvs can be found in this case, will return the better fit. """ matching_venvs = [] for venv_str in current_venvs: venv = json.loads(venv_str) # simple filter, need to have exactly same options and interpreter if venv.get('options') != options or venv.get('interpreter') != interpreter: continue # requirements complying: result can be None (no comply) or a score to later sort matching = self._venv_match(venv['installed'], requirements) if matching is not None: matching_venvs.append((matching, venv)) if not matching_venvs: return return self._select_better_fit(matching_venvs) def _select(self, current_venvs, requirements=None, interpreter='', uuid='', options=None): """Select which venv satisfy the received requirements.""" if uuid: logger.debug("Searching a venv by uuid: %s", uuid) venv = self._match_by_uuid(current_venvs, uuid) else: logger.debug("Searching a venv for: reqs=%s interpreter=%s options=%s", requirements, interpreter, options) venv = self._match_by_requirements(current_venvs, requirements, interpreter, options) if venv is None: logger.debug("No matching venv found :(") return logger.debug("Found a matching venv! %s", venv) return venv['metadata'] def get_venv(self, requirements=None, interpreter='', uuid='', options=None): """Find a venv that serves these requirements, if any.""" lines = self._read_cache() return self._select(lines, requirements, interpreter, uuid=uuid, options=options) def get_venvs_metadata(self): """Yield metadata of each existing venv.""" for line in self._read_cache(): yield json.loads(line)['metadata'] def store(self, installed_stuff, metadata, interpreter, options): """Store the virtualenv metadata for the indicated installed_stuff.""" new_content = { 'timestamp': int(time.mktime(time.localtime())), 'installed': installed_stuff, 'metadata': metadata, 'interpreter': interpreter, 'options': options } logger.debug("Storing installed=%s metadata=%s interpreter=%s options=%s", installed_stuff, metadata, interpreter, options) with filelock(self.lockpath): self._write_cache([json.dumps(new_content)], append=True) def remove(self, env_path): """Remove metadata for a given virtualenv from cache.""" with filelock(self.lockpath): cache = self._read_cache() logger.debug("Removing virtualenv from cache: %s" % env_path) lines = [ line for line in cache if json.loads(line).get('metadata', {}).get('env_path') != env_path ] self._write_cache(lines) def _read_cache(self): """Read virtualenv metadata from cache.""" if os.path.exists(self.filepath): with open(self.filepath, 'rt', encoding='utf8') as fh: lines = [x.strip() for x in fh] else: logger.debug("Index not found, starting empty") lines = [] return lines def _write_cache(self, lines, append=False): """Write virtualenv metadata to cache.""" mode = 'at' if append else 'wt' with open(self.filepath, mode, encoding='utf8') as fh: fh.writelines(line + '\n' for line in lines) ================================================ FILE: fades/envbuilder.py ================================================ # Copyright 2014-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tools to create, destroy and handle usage of virtual environments.""" import logging import os import pathlib import shutil from datetime import datetime, timezone from venv import EnvBuilder from uuid import uuid4 from fades import FadesError, REPO_PYPI, REPO_VCS from fades import helpers from fades.pipmanager import PipManager from fades.multiplatform import filelock logger = logging.getLogger(__name__) # UTC can be imported directly from datetime from Python 3.11 UTC = timezone.utc class _FadesEnvBuilder(EnvBuilder): """Create always a virtual environment. This is structured as a class mostly to take advantage of EnvBuilder, not because it's provides the best interface: external callers should just use module's ``create_env`` and ``destroy_env``. """ def __init__(self): basedir = helpers.get_basedir() self.env_path = os.path.join(basedir, str(uuid4())) self.env_bin_path = '' logger.debug("Env will be created at: %s", self.env_path) if os.environ.get("SNAP"): # running inside a snap: we need to avoid EnvBuilder ending up running ensurepip # because it doesn't work properly (it does a special magic to run the script # and ends up mixing external and internal pips) self.pip_installed = False else: # try to install pip using default machinery (which will work in a lot # of systems, noticeably it won't in some debians or ubuntus, like # Trusty; in that cases mark it to install manually later) try: import ensurepip # NOQA self.pip_installed = True except ImportError: self.pip_installed = False super().__init__(with_pip=self.pip_installed, symlinks=True) def create_with_external_venv(self, interpreter, options): """Create a virtual environment using the venv module externally.""" args = [interpreter, "-m", "venv", self.env_path] args.extend(options) if not self.pip_installed: args.insert(3, '--without-pip') try: helpers.logged_exec(args) except helpers.ExecutionError as error: error.dump_to_log(logger) raise FadesError("Failed to run venv module externally") except Exception as error: logger.exception("Error creating virtual environment: %s", error) raise FadesError("General error while running external venv") # XXX Facundo 2024-06-29: the helper uses pathlib; eventually everything will be # pathlib (see #435), so these translations will be cleaned up self.env_bin_path = str(helpers.get_env_bin_path(pathlib.Path(self.env_path))) def create_env(self, interpreter, is_current, options): """Create the virtual environment and return its info.""" venv_options = options['venv_options'] if is_current: # apply venv options logger.debug("Creating virtual environment internally; options=%s", venv_options) for option in venv_options: attrname = option[2:].replace("-", "_") # '--system-packgs' -> 'system_packgs' setattr(self, attrname, True) self.create(self.env_path) else: logger.debug( "Creating virtual environment with external venv; options=%s", venv_options) self.create_with_external_venv(interpreter, venv_options) logger.debug("env_bin_path: %s", self.env_bin_path) # Re check if pip was installed (supporting both binary and .exe for Windows) pip_bin = os.path.join(self.env_bin_path, "pip") pip_exe = os.path.join(self.env_bin_path, "pip.exe") if not (os.path.exists(pip_bin) or os.path.exists(pip_exe)): logger.debug("pip isn't installed in the venv, setting pip_installed=False") self.pip_installed = False return self.env_path, self.env_bin_path, self.pip_installed def post_setup(self, context): """Get the bin path from context.""" self.env_bin_path = context.bin_path def create_venv(requested_deps, interpreter, is_current, options, pip_options, avoid_pip_upgrade): """Create a new virtualvenv with the requirements of this script.""" # create virtual environment env = _FadesEnvBuilder() env_path, env_bin_path, pip_installed = env.create_env(interpreter, is_current, options) venv_data = {} venv_data['env_path'] = env_path venv_data['env_bin_path'] = env_bin_path venv_data['pip_installed'] = pip_installed # install deps installed = {} for repo in requested_deps.keys(): if repo in (REPO_PYPI, REPO_VCS): mgr = PipManager( env_bin_path, pip_installed=pip_installed, options=pip_options, avoid_pip_upgrade=avoid_pip_upgrade) else: logger.warning("Install from %r not implemented", repo) continue installed[repo] = {} repo_requested = requested_deps[repo] logger.debug("Installing dependencies for repo %r: requested=%s", repo, repo_requested) for dependency in repo_requested: try: mgr.install(dependency) except Exception: logger.debug("Installation Step failed, removing virtual environment") destroy_venv(env_path) raise FadesError('Dependency installation failed') if repo == REPO_VCS: # no need to request the installed version, as we'll always compare # to the url itself project = dependency.url version = None else: # always store the installed dependency, as in the future we'll select the venv # based on what is installed, not what used requested (remember that user may # request >, >=, etc!) project = dependency.name version = mgr.get_version(project) installed[repo][project] = version logger.debug("Installed dependencies: %s", installed) return venv_data, installed def destroy_venv(env_path, venvscache=None): """Destroy a venv.""" # remove the venv itself in disk logger.debug("Destroying virtual environment at: %s", env_path) shutil.rmtree(env_path, ignore_errors=True) # remove venv from cache if venvscache is not None: venvscache.remove(env_path) class UsageManager: """Class to handle usage file and venv cleanning.""" def __init__(self, stat_file_path, venvscache): """Init.""" self.stat_file_path = stat_file_path self.stat_file_lock = stat_file_path + '.lock' self.venvscache = venvscache self._create_initial_usage_file_if_not_exists() def store_usage_stat(self, venv_data, cache): """Log an usage record for venv_data.""" with open(self.stat_file_path, 'at') as f: self._write_venv_usage(f, venv_data) def _create_initial_usage_file_if_not_exists(self): if not os.path.exists(self.stat_file_path): existing_venvs = self.venvscache.get_venvs_metadata() with open(self.stat_file_path, 'wt') as f: for venv_data in existing_venvs: self._write_venv_usage(f, venv_data) def _write_venv_usage(self, file_, venv_data): _, uuid = os.path.split(venv_data['env_path']) file_.write('{} {}\n'.format(uuid, self._datetime_to_str(datetime.now(UTC)))) def _datetime_to_str(self, datetime_): return datetime.strftime(datetime_, "%Y-%m-%dT%H:%M:%S.%f") def _str_to_datetime(self, str_): return datetime.strptime(str_, "%Y-%m-%dT%H:%M:%S.%f") def clean_unused_venvs(self, max_days_to_keep): """Compact usage stats and remove venvs. This method loads the complete file usage in memory, for every venv compact all records in one (the lastest), updates this info for every env deleted and, finally, write the entire file to disk. If something failed during this steps, usage file remains unchanged and can contain some data about some deleted env. This is not a problem, the next time this function it's called, this records will be deleted. """ with filelock(self.stat_file_lock): now = datetime.now(UTC) venvs_dict = self._get_compacted_dict_usage_from_file() for venv_uuid, usage_date in venvs_dict.copy().items(): usage_date = self._str_to_datetime(usage_date) if (now - usage_date).days > max_days_to_keep: # remove venv from usage dict del venvs_dict[venv_uuid] venv_meta = self.venvscache.get_venv(uuid=venv_uuid) if venv_meta is None: # if meta isn't found means that something had failed previously and # usage_file wasn't updated. continue env_path = venv_meta['env_path'] logger.info("Destroying virtual environment at: %s", env_path) destroy_venv(env_path, self.venvscache) self._write_compacted_dict_usage_to_file(venvs_dict) def _get_compacted_dict_usage_from_file(self): all_lines = open(self.stat_file_path).readlines() return dict(x.split() for x in all_lines) def _write_compacted_dict_usage_to_file(self, dict_usage): with open(self.stat_file_path, 'wt') as file_: for uuid, date in dict_usage.items(): file_.write('{} {}\n'.format(uuid, date)) ================================================ FILE: fades/file_options.py ================================================ # Copyright 2016-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Parse fades options from config files.""" import logging import os from configparser import ConfigParser, NoSectionError from fades.helpers import get_confdir logger = logging.getLogger(__name__) CONFIG_FILES = ("/etc/fades/fades.ini", os.path.join(get_confdir(), 'fades.ini'), ".fades.ini") MERGEABLE_CONFIGS = ("dependency", "pip_options", "venv-options") def options_from_file(args): """Get a argparse.Namespace and return it updated with options from config files. Config files will be parsed with priority equal to his order in CONFIG_FILES. """ logger.debug("updating options from config files") updated_from_file = [] for config_file in CONFIG_FILES: logger.debug("updating from: %s", config_file) parser = ConfigParser() parser.read(config_file) try: items = parser.items('fades') except NoSectionError: continue for config_key, config_value in items: if config_value in ['true', 'false']: config_value = config_value == 'true' if config_key in MERGEABLE_CONFIGS: current_value = getattr(args, config_key, []) if current_value is None: current_value = [] current_value.append(config_value) setattr(args, config_key, current_value) if not getattr(args, config_key, False) or config_key in updated_from_file: # By default all 'store-true' arguments are False. So we only # override them if they are False. If they are True means that the # user is setting those on the CLI. setattr(args, config_key, config_value) updated_from_file.append(config_key) logger.debug("updating %s to %s from file settings", config_key, config_value) return args ================================================ FILE: fades/helpers.py ================================================ # Copyright 2014-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """A collection of utilities for fades.""" import os import sys import json import logging import subprocess import tempfile from http.server import HTTPStatus from urllib import request, parse from urllib.error import HTTPError from packaging.requirements import Requirement from packaging.version import Version from fades import FadesError, _version logger = logging.getLogger(__name__) # command to retrieve the version from an external Python SHOW_VERSION_CMD = """ import sys, json d = dict(path=sys.executable) d.update(zip('major minor micro releaselevel serial'.split(), sys.version_info)) print(json.dumps(d)) """ # the url to query PyPI for project versions BASE_PYPI_URL = 'https://pypi.org/pypi/{name}/json' BASE_PYPI_URL_WITH_VERSION = 'https://pypi.org/pypi/{name}/{version}/json' # prefix for all stdout lines when running a command STDOUT_LOG_PREFIX = ":: " # env var name provided by snappy where process can read/write; this path already includes # 'fades' in it, it's a different dir for each user, and accessable by different versions of fades SNAP_BASEDIR_NAME = 'SNAP_USER_COMMON' class ExecutionError(Exception): """Execution of subprocess ended not in 0.""" def __init__(self, retcode, cmd, collected_stdout): """Init.""" self._retcode = retcode self._cmd = cmd self._collected_stdout = collected_stdout super().__init__() def dump_to_log(self, logger): """Send the cmd info and collected stdout to logger.""" logger.error("Execution ended in %s for cmd %s", self._retcode, self._cmd) for line in self._collected_stdout: logger.error(STDOUT_LOG_PREFIX + line) def logged_exec(cmd): """Execute a command, redirecting the output to the log.""" logger = logging.getLogger('fades.exec') logger.debug("Executing external command: %r", cmd) p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) stdout = [] for line in p.stdout: line = line[:-1] stdout.append(line) logger.debug(STDOUT_LOG_PREFIX + line) retcode = p.wait() if retcode: raise ExecutionError(retcode, cmd, stdout) return stdout def _get_basedirectory(): from xdg import BaseDirectory return BaseDirectory def _get_specific_dir(dir_type): """Get a specific directory, using some XDG base, with sensible default.""" if SNAP_BASEDIR_NAME in os.environ: logger.debug("Getting base dir information from SNAP_BASEDIR_NAME env var.") direct = os.path.join(os.environ[SNAP_BASEDIR_NAME], dir_type) else: try: basedirectory = _get_basedirectory() except ImportError: logger.debug("Using last resort base dir: ~/.fades") from os.path import expanduser direct = os.path.join(expanduser("~"), ".fades") else: xdg_attrib = 'xdg_{}_home'.format(dir_type) base = getattr(basedirectory, xdg_attrib) direct = os.path.join(base, 'fades') if not os.path.exists(direct): os.makedirs(direct) return direct def get_basedir(): """Get the base fades directory, from xdg or kinda hardcoded.""" return _get_specific_dir('data') def get_confdir(): """Get the config fades directory, from xdg or kinda hardcoded.""" return _get_specific_dir('config') def _get_interpreter_info(interpreter=None): """Return the interpreter's full path using pythonX.Y format.""" if interpreter is None: # If interpreter is None by default returns the current interpreter data. major, minor = sys.version_info[:2] executable = sys.executable else: args = [interpreter, '-c', SHOW_VERSION_CMD] try: requested_interpreter_info = logged_exec(args) except Exception as error: logger.error("Error getting requested interpreter version: %s", error) raise FadesError("Could not get interpreter version") requested_interpreter_info = json.loads(requested_interpreter_info[0]) executable = requested_interpreter_info['path'] major = requested_interpreter_info['major'] minor = requested_interpreter_info['minor'] if executable[-1].isdigit(): executable = executable.split(".")[0][:-1] interpreter = "{}{}.{}".format(executable, major, minor) return interpreter def get_interpreter_version(requested_interpreter): """Return a 'sanitized' interpreter and indicates if it is the current one.""" logger.debug('Getting interpreter version for: %s', requested_interpreter) current_interpreter = _get_interpreter_info() logger.debug('Current interpreter is %s', current_interpreter) if requested_interpreter is None: return (current_interpreter, True) requested_interpreter = _get_interpreter_info(requested_interpreter) is_current = requested_interpreter == current_interpreter logger.debug('Interpreter=%s. It is the same as fades?=%s', requested_interpreter, is_current) return (requested_interpreter, is_current) def get_latest_version_number(project_name): """Return latest version of a package.""" try: raw = request.urlopen(BASE_PYPI_URL.format(name=project_name)).read() except HTTPError as error: logger.warning("Network error. Error: %s", error) raise error try: data = json.loads(raw.decode("utf8")) latest_version = data["info"]["version"] return latest_version except (KeyError, ValueError) as error: # malformed json or empty string logger.error("Could not get the version of the package. Error: %s", error) raise error def check_pypi_updates(dependencies): """Return a list of dependencies to upgrade.""" dependencies_up_to_date = [] for dependency in dependencies.get('pypi', []): # get latest version from PyPI api try: latest_version = Version(get_latest_version_number(dependency.name)) except Exception as error: logger.warning("--check-updates command will be aborted. Error: %s", error) return dependencies # get required version if dependency.specifier: spec = list(dependency.specifier)[0] required_version = Version(spec.version) dependencies_up_to_date.append(dependency) if latest_version > required_version: logger.info("There is a new version of %s: %s", dependency.name, latest_version) elif latest_version < required_version: logger.warning("The requested version for %s is greater " "than latest found in PyPI: %s", dependency.name, latest_version) else: logger.info("The requested version for %s is the latest one in PyPI: %s", dependency.name, latest_version) else: name_plus = "{}=={}".format(dependency.name, latest_version) dependencies_up_to_date.append(Requirement(name_plus)) logger.info("The latest version of %r is %s and will use it.", dependency.name, latest_version) dependencies["pypi"] = dependencies_up_to_date return dependencies def _pypi_head_package(dependency): """Hit pypi with a http HEAD to check if pkg_name exists.""" if dependency.specifier: spec = list(dependency.specifier)[0] version = spec.version url = BASE_PYPI_URL_WITH_VERSION.format(name=dependency.name, version=version) else: url = BASE_PYPI_URL.format(name=dependency.name) logger.debug("Doing HEAD requests against %s", url) req = request.Request(url, method='HEAD') try: response = request.urlopen(req) except HTTPError as http_error: if http_error.code == HTTPStatus.NOT_FOUND: return False else: raise if response.status == HTTPStatus.OK: logger.debug("%r exists in PyPI.", dependency) else: # Maybe we are getting somethink like a redirect. In this case we are only # warning to the user and trying to install the dependency. # In the worst scenery fades will fail to install it. logger.warning("Got a (unexpected) HTTP_STATUS=%r and reason=%r checking if %r exists", response.status, response.reason, dependency) return True def check_pypi_exists(dependencies): """Check if the indicated dependencies actually exists in pypi.""" for dependency in dependencies.get('pypi', []): logger.debug("Checking if %r exists in PyPI", dependency) try: exists = _pypi_head_package(dependency) except Exception as error: logger.error("Error checking %s in PyPI: %r", dependency, error) raise FadesError("Could not check if dependency exists in PyPI") else: if not exists: logger.error("%s doesn't exists in PyPI.", dependency) return False return True class _ScriptDownloader: """Grouping of different backends downloaders.""" # a user-agent for hitting the network USER_AGENT = "fades/{} (https://github.com/PyAr/fades/)".format(_version.__version__) HEADERS_PLAIN = { 'Accept': 'text/plain', 'User-Agent': USER_AGENT, } HEADERS_JSON = { 'Accept': 'application/json', 'User-Agent': USER_AGENT, } # simple network locations to name map NETLOCS = { 'linkode.org': 'linkode', 'pastebin.com': 'pastebin', 'gist.github.com': 'gist', } def __init__(self, url): """Init.""" self.url = url self.name = self._decide() def _decide(self): """Find out which method should be applied to download that URL.""" netloc = parse.urlparse(self.url).netloc name = self.NETLOCS.get(netloc, 'raw') return name def get(self): """Get the script content from the URL using the decided downloader.""" method_name = "_download_" + self.name method = getattr(self, method_name) return method() def _download_raw(self, url=None): """Download content from URL directly.""" if url is None: url = self.url req = request.Request(url, headers=self.HEADERS_PLAIN) resp = request.urlopen(req) # check if the response url is different than the original one; in this case we had # redirected, and we need to pass the new url response through the proper # pastebin-dependant adapter, so recursively go into another _ScriptDownloader if resp.geturl() != url: new_url = resp.geturl() downloader = _ScriptDownloader(new_url) logger.info( "Download redirect detect, now downloading from %r using %r downloader", new_url, downloader.name) return downloader.get() # simple non-redirect response return resp.read().decode("utf8") def _download_linkode(self): """Download content from Linkode pastebin.""" # build the API url linkode_id = self.url.split("/")[-1] if linkode_id.startswith("#"): linkode_id = linkode_id[1:] url = "https://linkode.org/api/1/linkodes/" + linkode_id req = request.Request(url, headers=self.HEADERS_JSON) resp = request.urlopen(req) raw = resp.read() data = json.loads(raw.decode("utf8")) content = data['content'] return content def _download_pastebin(self): """Download content from Pastebin itself.""" paste_id = self.url.split("/")[-1] url = "https://pastebin.com/raw/" + paste_id return self._download_raw(url) def _download_gist(self): """Download content from github's pastebin.""" parts = parse.urlparse(self.url) url = "https://gist.github.com" + parts.path + "/raw" return self._download_raw(url) def download_remote_script(url): """Download the content of a remote script to a local temp file.""" temp_fh = tempfile.NamedTemporaryFile('wt', encoding='utf8', suffix=".py", delete=False) downloader = _ScriptDownloader(url) logger.info( "Downloading remote script from %r (using %r downloader) to %r", url, downloader.name, temp_fh.name) content = downloader.get() temp_fh.write(content) temp_fh.close() return temp_fh.name def get_env_bin_path(base_env_path): """Find and return the environment's binary path in a multiplatformy way.""" for subdir in ("bin", "Scripts"): binpath = base_env_path / subdir if binpath.exists(): return binpath raise ValueError(f"Binary subdir not found in {base_env_path!r}") ================================================ FILE: fades/logger.py ================================================ # Copyright 2014-2018 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Logging set up.""" import logging import logging.handlers import os.path from fades._version import __version__ FMT_SIMPLE = "*** fades *** %(asctime)s %(levelname)-8s %(message)s" FMT_DETAILED = "*** fades *** %(asctime)s %(name)-18s %(levelname)-8s %(message)s" FMT_SYSLOG = "[%(process)d] %(name)-18s %(levelname)-8s %(message)s" SALUTATION = "Hi! This is fades {}, automatically managing your dependencies".format(__version__) class SalutingStreamHandler(logging.StreamHandler): """A handler that salutes once before polluting user screen. Note that the salutation is done in INFO level, to respect "verbose" modifiers. """ def __init__(self, logger): """Init.""" super().__init__() self._already_saluted = False self._logger = logger def emit(self, record): """Call father's emit, but salute first (just once).""" if not self._already_saluted: self._already_saluted = True self._logger.info(SALUTATION) super().emit(record) def set_up(verbose, quiet): """Set up the logging.""" logger = logging.getLogger('fades') logger.setLevel(logging.DEBUG) # select logging level according to user desire; also use a simpler # formatting for non-verbose logging if verbose: log_level = logging.DEBUG log_format = FMT_DETAILED elif quiet: log_level = logging.WARNING log_format = FMT_SIMPLE else: log_level = logging.INFO log_format = FMT_SIMPLE # all to the stdout handler = SalutingStreamHandler(logger) handler.setLevel(log_level) logger.addHandler(handler) formatter = logging.Formatter(log_format) handler.setFormatter(formatter) # and to the syslog for syslog_path in ('/dev/log', '/var/run/syslog'): if not os.path.exists(syslog_path): continue try: handler = logging.handlers.SysLogHandler(address=syslog_path) except Exception: # silently ignore that the user doesn't have a syslog active; can # see all the info with "-v" anyway pass else: logger.addHandler(handler) formatter = logging.Formatter(FMT_SYSLOG) handler.setFormatter(formatter) break return logger ================================================ FILE: fades/main.py ================================================ # Copyright 2014-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General # Public License version 3, as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. # If not, see . # # For further info, check https://github.com/PyAr/fades """Main 'fades' modules.""" import argparse import logging import os import platform import signal import sys import subprocess import tempfile import fades from fades import ( FadesError, cache, envbuilder, file_options, helpers, parsing, pipmanager, pkgnamesdb, ) from fades.logger import set_up as logger_set_up # Get the logger here; it will be properly setup at bootstrap, but can be used from # the rest of the module just fine logger = logging.getLogger('fades') # the signals to redirect to the child process (note: only these are # allowed in Windows, see 'signal' doc). REDIRECTED_SIGNALS = [ signal.SIGABRT, signal.SIGFPE, signal.SIGILL, signal.SIGINT, signal.SIGSEGV, signal.SIGTERM, ] HELP_EPILOG = """ The "child program" is the script that fades will execute. It's an optional parameter, it will be the first thing received by fades that is not a parameter. If no child program is indicated, a Python interactive interpreter will be opened. The "child options" (everything after the child program) are parameters passed as is to the child program. """ AUTOIMPORT_HEADER = """ import sys print("Python {} on {}".format(sys.version, sys.platform)) print('Type "help", "copyright", "credits" or "license" for more information.') """ AUTOIMPORT_MOD_IMPORTER = """ try: import {module} except ImportError: print("::fades:: FAILED to autoimport {module!r}") else: print("::fades:: automatically imported {module!r}") """ AUTOIMPORT_MOD_SKIPPING = ( """print("::fades:: autoimport skipped because not a PyPI package: {dependency!r}")\n""") def get_autoimport_scriptname(dependencies, is_ipython): """Return the path of script that will import dependencies for interactive mode. The script has: - a safe import of the dependencies, taking in consideration that the module may be named differently than the package, and printing a message accordingly - if regular Python, also print the normal interactive interpreter first information lines, that are not shown when starting it with `-i` (but IPython shows them anyway). """ fd, tempfilepath = tempfile.mkstemp(prefix='fadesinit-', suffix='.py') fh = os.fdopen(fd, 'wt', encoding='utf8') if not is_ipython: fh.write(AUTOIMPORT_HEADER) for repo, dependencies in dependencies.items(): for dependency in dependencies: if repo == fades.REPO_PYPI: package = dependency.name if is_ipython and package == 'ipython': # Ignore this artificially added dependency. continue module = pkgnamesdb.PACKAGE_TO_MODULE.get(package, package) fh.write(AUTOIMPORT_MOD_IMPORTER.format(module=module)) else: fh.write(AUTOIMPORT_MOD_SKIPPING.format(dependency=dependency)) fh.close() return tempfilepath def consolidate_dependencies(needs_ipython, child_program, requirement_files, manual_dependencies): """Parse files, get deps and merge them. Deps read later overwrite those read earlier.""" if needs_ipython: logger.debug("Adding ipython dependency because --ipython was detected") ipython_dep = parsing.parse_manual(['ipython']) else: ipython_dep = {} if child_program: srcfile_deps = parsing.parse_srcfile(child_program) logger.debug("Dependencies from source file: %s", srcfile_deps) docstring_deps = parsing.parse_docstring(child_program) logger.debug("Dependencies from docstrings: %s", docstring_deps) else: srcfile_deps = {} docstring_deps = {} all_dependencies = [ipython_dep, srcfile_deps, docstring_deps] if requirement_files is not None: for rf_path in requirement_files: rf_deps = parsing.parse_reqfile(rf_path) logger.debug('Dependencies from requirements file %r: %s', rf_path, rf_deps) all_dependencies.append(rf_deps) manual_deps = parsing.parse_manual(manual_dependencies) logger.debug("Dependencies from parameters: %s", manual_deps) all_dependencies.append(manual_deps) # Merge dependencies indicated_deps = {} for dep in all_dependencies: for repo, info in dep.items(): indicated_deps.setdefault(repo, set()).update(info) return indicated_deps def decide_child_program(args_executable, args_module, args_child_program): """Decide which the child program really is (if any).""" if args_executable: # If --exec given, check that it's just the executable name or an absolute path; # relative paths are forbidden (as the location of the venv should not be known). if os.path.sep in args_child_program and args_child_program[0] != os.path.sep: logger.error( "The parameter to --exec must be a file name (to be found " "inside venv's bin directory), not a file path: %r", args_child_program) raise FadesError("File path given to --exec parameter") # indicated --execute, local and not analyzable for dependencies analyzable_child_program = None child_program = args_child_program elif args_module: # If --module given, the module may be installed (nothing can be really checked), # but surely it's not used as a source for dependencies. analyzable_child_program = None child_program = args_child_program elif args_child_program is not None: # normal case, the child program is to be analyzed (being it local or remote) if args_child_program.startswith(("http://", "https://")): args_child_program = helpers.download_remote_script(args_child_program) else: if not os.access(args_child_program, os.R_OK): logger.error("'%s' not found. If you want to run an executable " "file from a library installed in the virtualenv " "check the `--exec` option in the help.", args_child_program) raise FadesError("child program not found.") analyzable_child_program = args_child_program child_program = args_child_program else: # not indicated executable, not child program, "interpreter" mode analyzable_child_program = None child_program = None return analyzable_child_program, child_program def detect_inside_virtualenv(prefix, real_prefix, base_prefix): """Tell if fades is running inside a virtualenv. The params 'real_prefix' and 'base_prefix' may be None. This is copied from pip code (slightly modified), see https://github.com/pypa/pip/blob/281eb61b09d87765d7c2b92f6982b3fe76ccb0af/ pip/locations.py#L39 """ if os.environ.get("SNAP"): # snaps under core20 are really virtualenvs but we have full control of the # system layout, "this is fine" (), skip this control return False if real_prefix is not None: return True if base_prefix is None: return False # if prefix is different than base_prefix, it's a venv return prefix != base_prefix def go(): """Make the magic happen.""" parser = argparse.ArgumentParser( prog='fades', epilog=HELP_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( '-V', '--version', action='store_true', help="show version and info about the system, and exit") parser.add_argument( '-d', '--dependency', action='append', help="specify dependencies through command line (this option can be used multiple times)") parser.add_argument( '-r', '--requirement', action='append', help="indicate files to read dependencies from (this option can be used multiple times)") parser.add_argument( '-p', '--python', action='store', help="specify the Python interpreter to use; the default is: {}".format(sys.executable)) parser.add_argument( '-i', '--ipython', action='store_true', help="use IPython shell when in interactive mode") parser.add_argument( '--system-site-packages', action='store_true', default=False, help="give the virtual environment access to the system site-packages dir.") parser.add_argument( '--venv-options', action='append', default=[], help="extra options to be supplied to the venv module " "(this option can be used multiple times)") parser.add_argument( '-U', '--check-updates', action='store_true', help="check for packages updates") parser.add_argument( '--no-precheck-availability', action='store_true', help="don't check if the packages exists in PyPI before actually try to install them") parser.add_argument( '--pip-options', action='append', default=[], help="extra options to be supplied to pip (this option can be used multiple times)") parser.add_argument( '--python-options', action='append', default=[], help="extra options to be supplied to python (this option can be used multiple times)") parser.add_argument( '--rm', dest='remove', metavar='UUID', help="remove a virtualenv by UUID; see --where option to easily find out the UUID") parser.add_argument( '--clean-unused-venvs', action='store', help="remove venvs that haven't been used for more than the indicated days and compact " "usage stats file (all this takes place at the beginning of the execution)") parser.add_argument( '--where', '--get-venv-dir', action='store_true', help="show the virtualenv base directory (including the venv's UUID) and quit") parser.add_argument( '-a', '--autoimport', action='store_true', help="automatically import the specified dependencies in the interactive mode " "(ignored otherwise).") parser.add_argument( '--freeze', action='store', metavar='FILEPATH', help="dump all the dependencies and its versions to the specified filepath " "(operating normally beyond that)") parser.add_argument( '--avoid-pip-upgrade', action='store_true', help="disable the automatic pip upgrade that happens after the virtualenv is created " "and before the dependencies begin to be installed.") mutexg = parser.add_mutually_exclusive_group() mutexg.add_argument( '-v', '--verbose', action='store_true', help="send all internal debugging lines to stderr, which may be very " "useful to debug any problem that may arise") mutexg.add_argument( '-q', '--quiet', action='store_true', help="don't show anything (unless it has a real problem), so the " "original script stderr is not polluted at all") mutexg = parser.add_mutually_exclusive_group() mutexg.add_argument( '-x', '--exec', dest='executable', action='store_true', help="execute the child_program (must be present) in the context of the virtualenv") mutexg.add_argument( '-m', '--module', action='store_true', help="run library module as a script (same behaviour than Python's -m parameter)") parser.add_argument('child_program', nargs='?', default=None) parser.add_argument('child_options', nargs=argparse.REMAINDER) cli_args = parser.parse_args() # update args from config file (if needed). args = file_options.options_from_file(cli_args) # validate input, parameters, and support some special options if args.version: print("Running 'fades' version", fades.__version__) print(" Python:", sys.version_info) print(" System:", platform.platform()) return 0 # The --exec and --module flags needs child_program to exist (this is not handled at # argparse level because it's easier to collect the executable as the # normal child_program, so everything after that are parameteres # considered for the executable itself, not for fades). if args.executable and not args.child_program: parser.print_usage() print("fades: error: argument -x/--exec needs child_program to be present") return -1 if args.module and not args.child_program: parser.print_usage() print("fades: error: argument -m/--module needs child_program (module) to be present") return -1 # set up the logger and dump basic version info logger_set_up(args.verbose, args.quiet) logger.debug("Running Python %s on %r", sys.version_info, platform.platform()) logger.debug("Starting fades v. %s", fades.__version__) logger.debug("Arguments: %s", args) # verify that the module is NOT being used from a virtualenv _real_prefix = getattr(sys, 'real_prefix', None) _base_prefix = getattr(sys, 'base_prefix', None) if detect_inside_virtualenv(sys.prefix, _real_prefix, _base_prefix): logger.error( "fades is running from inside a virtualenv (%r), which is not supported", sys.prefix) raise FadesError("Cannot run from a virtualenv") if args.verbose and args.quiet: logger.warning("Overriding 'quiet' option ('verbose' also requested)") # start the virtualenvs manager venvscache = cache.VEnvsCache(os.path.join(helpers.get_basedir(), 'venvs.idx')) # start usage manager usage_manager = envbuilder.UsageManager( os.path.join(helpers.get_basedir(), 'usage_stats'), venvscache) if args.clean_unused_venvs: try: max_days_to_keep = int(args.clean_unused_venvs) except ValueError: logger.error("clean_unused_venvs must be an integer.") raise FadesError('clean_unused_venvs not an integer') usage_manager.clean_unused_venvs(max_days_to_keep) return 0 uuid = args.remove if uuid: venv_data = venvscache.get_venv(uuid=uuid) if venv_data: # remove this venv from the cache env_path = venv_data.get('env_path') if env_path: envbuilder.destroy_venv(env_path, venvscache) else: logger.warning( "Invalid 'env_path' found in virtualenv metadata: %r. " "Not removing virtualenv.", env_path) else: logger.warning('No virtualenv found with uuid: %s.', uuid) return 0 # decided which the child program really is analyzable_child_program, child_program = decide_child_program( args.executable, args.module, args.child_program) # Group and merge dependencies indicated_deps = consolidate_dependencies( args.ipython, analyzable_child_program, args.requirement, args.dependency) # Check for packages updates if args.check_updates: helpers.check_pypi_updates(indicated_deps) # get the interpreter version requested for the child_program interpreter, is_current = helpers.get_interpreter_version(args.python) # options pip_options = args.pip_options # pip_options mustn't store. python_options = args.python_options options = {} options['venv_options'] = args.venv_options if args.system_site_packages: options['venv_options'].append("--system-site-packages") create_venv = False venv_data = venvscache.get_venv(indicated_deps, interpreter, uuid, options) if venv_data: env_path = venv_data['env_path'] # A venv was found in the cache check if its valid or re-generate it. if not os.path.exists(env_path): logger.warning("Missing directory (the virtualenv will be re-created): %r", env_path) venvscache.remove(env_path) create_venv = True else: create_venv = True if create_venv: # Check if the requested packages exists in pypi. if not args.no_precheck_availability and indicated_deps.get('pypi'): logger.info( "Checking the availabilty of dependencies in PyPI. " "You can use '--no-precheck-availability' to avoid it.") if not helpers.check_pypi_exists(indicated_deps): logger.error("An indicated dependency doesn't exist. Exiting") raise FadesError("Required dependency does not exist") # Create a new venv venv_data, installed = envbuilder.create_venv( indicated_deps, args.python, is_current, options, pip_options, args.avoid_pip_upgrade) # store this new venv in the cache venvscache.store(installed, venv_data, interpreter, options) if args.where: # all it was requested is the virtualenv's path, show it and quit (don't run anything) print(venv_data['env_path']) return 0 if args.freeze: # beyond all the rest of work, dump the dependencies versions to a file mgr = pipmanager.PipManager(venv_data['env_bin_path']) mgr.freeze(args.freeze) # run forest run!! python_exe = 'ipython' if args.ipython else 'python' python_exe = os.path.join(venv_data['env_bin_path'], python_exe) # add the virtualenv /bin path to the child PATH. environ_path = venv_data['env_bin_path'] if 'PATH' in os.environ: environ_path += os.pathsep + os.environ['PATH'] os.environ['PATH'] = environ_path # store usage information usage_manager.store_usage_stat(venv_data, venvscache) if child_program is None: interactive = True cmd = [python_exe] + python_options # get possible extra python options and environement for auto import if indicated_deps and args.autoimport: temp_scriptpath = get_autoimport_scriptname(indicated_deps, args.ipython) cmd += ['-i', temp_scriptpath] logger.debug("Calling the interactive Python interpreter: %s", cmd) proc = subprocess.Popen(cmd) else: interactive = False if args.executable: # Build the exec path relative to 'bin' dir; note that if child_program's path # is absolute (starting with '/') the resulting exec_path will be just it, # which is something fades supports exec_path = os.path.join(venv_data['env_bin_path'], child_program) cmd = [exec_path] elif args.module: cmd = [python_exe, '-m'] + python_options + [child_program] else: cmd = [python_exe] + python_options + [child_program] # Incorporate the child options, always at the end, log and run. cmd += args.child_options logger.debug("Calling %s", cmd) try: proc = subprocess.Popen(cmd) except FileNotFoundError: logger.error("Command not found: %s", child_program) raise FadesError("Command not found") def _signal_handler(signum, _): """Handle signals received by parent process, send them to child. The only exception is CTRL-C, that is generated *from* the interactive interpreter (it's a keyboard combination!), so we swallow it for the interpreter to not see it twice. """ if interactive and signum == signal.SIGINT: logger.debug("Swallowing signal %s", signum) else: logger.debug("Redirecting signal %s to child", signum) os.kill(proc.pid, signum) # redirect the useful signals for s in REDIRECTED_SIGNALS: signal.signal(s, _signal_handler) # wait child to finish, end rc = proc.wait() if rc: logger.debug("Child process not finished correctly: returncode=%d", rc) return rc ================================================ FILE: fades/multiplatform.py ================================================ # Copyright 2016 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General # Public License version 3, as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. # If not, see . # # For further info, check https://github.com/PyAr/fades """Platform agnostic collection of utilities.""" import os from contextlib import contextmanager try: import fcntl @contextmanager def filelock(filepath): """Context manager to lock over a file using best method: fcntl.""" with open(filepath, 'w') as fh: fcntl.flock(fh, fcntl.LOCK_EX) yield fcntl.flock(fh, fcntl.LOCK_UN) if os.path.exists(filepath): os.remove(filepath) except ImportError: import time @contextmanager def filelock(filepath): """Context manager to lock over a file where fcntl doesn't exist.""" try: while True: try: with open(filepath, "x"): yield break except FileExistsError: time.sleep(.5) finally: if os.path.exists(filepath): os.remove(filepath) ================================================ FILE: fades/parsing.py ================================================ # Copyright 2014-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Script parsing to get needed dependencies.""" import logging import os import re from packaging.requirements import Requirement from packaging.version import Version from fades import REPO_PYPI, REPO_VCS from fades.pkgnamesdb import MODULE_TO_PACKAGE logger = logging.getLogger(__name__) class _VCSSpecifier: """A simple specifier that works with VCSDependency.""" def contains(self, other): """VCS dependency does not handle versions.""" return other is None class VCSDependency: """A dependency object for VCS urls (git, bzr, etc.). It stores as unique identifier the whole URL; there may be a little inefficiency because we may consider as different two urls for same project but using different transports, but it's a small price for not needing to parse and analyze url parts. """ def __init__(self, url): """Init.""" self.url = self.name = self.project_name = self.version = url self.specifier = _VCSSpecifier() def __str__(self): """Return the URL as this is the interface to get what pip will use.""" return self.url def __repr__(self): """Repr.""" return "".format(self.url) def __eq__(self, other): """Tell if one VCSDependency is equal to other.""" if not isinstance(other, VCSDependency): return False return self.url == other.url def __hash__(self): """Pair to __eq__ to make this hashable.""" return hash(self.url) class NameVerDependency: """A dependency indicated by name and version.""" def __init__(self, name, version): self.name = name self.version = Version(version) def __eq__(self, other): return self.name == other.name and self.version == other.version def __hash__(self): return hash((self.name, self.version)) def __lt__(self, other): assert not isinstance(self.version, str) return (self.name, self.version) < (other.name, other.version) def parse_fade_requirement(text): """Return a requirement and repo from the given text, already parsed and converted.""" text = text.strip() if "::" in text: repo_raw, requirement = text.split("::", 1) try: repo = {'pypi': REPO_PYPI, 'vcs': REPO_VCS}[repo_raw] except KeyError: logger.warning("Not understood fades repository: %r", repo_raw) return else: if ":" in text and "/" in text: repo = REPO_VCS else: repo = REPO_PYPI requirement = text if repo == REPO_VCS: dependency = VCSDependency(requirement) else: dependency = Requirement(requirement) return repo, dependency def _parse_content(fh): """Parse the content of a script to find marked dependencies.""" content = iter(fh) deps = {} for line in content: # quickly discard most of the lines if 'fades' not in line: continue # discard other string with 'fades' that isn't a comment if '#' not in line: continue # assure that it's a well commented line and no other stuff line = line.strip() index_of_last_fades = line.rfind('fades') index_of_first_hash = line.index('#') # discard when fades does not appear after # if index_of_first_hash > index_of_last_fades: continue import_part, fades_part = line.rsplit("#", 1) # discard other comments in the same line that aren't for fades if "fades" not in fades_part: import_part, fades_part = import_part.rsplit("#", 1) fades_part = fades_part.strip() if not fades_part.startswith("fades"): continue if not import_part: # the fades comment was done at the beginning of the line, # which means that the import info is in the next one import_part = next(content).strip() if import_part.startswith('#'): continue # Get the module. import_tokens = import_part.split() if import_tokens[0] == 'import': module_path = import_tokens[1] elif import_tokens[0] == 'from' and import_tokens[2] == 'import': module_path = import_tokens[1] else: logger.debug("Not understood import info: %s", import_tokens) continue module = module_path.split(".")[0] # The package has the same name (most of the times! if fades knows the conversion, use it). if module in MODULE_TO_PACKAGE: package = MODULE_TO_PACKAGE[module] else: package = module # To match the "safe" name package = package.replace('_', '-') # get the fades info after 'fades' mark, if any if len(fades_part) == 5 or fades_part[5:].strip()[0] in "<>=!": # just the 'fades' mark, and maybe a version specification, the requirement is what # was imported (maybe with that version comparison) requirement = package + fades_part[5:] elif fades_part[5] != " ": # starts with fades but it's part of a longer weird word logger.warning("Not understood fades info: %r", fades_part) continue else: # more complex stuff, to be parsed as a normal requirement requirement = fades_part[5:] # parse and convert the requirement parsed_req = parse_fade_requirement(requirement) if parsed_req is None: continue repo, dependency = parsed_req deps.setdefault(repo, []).append(dependency) return deps def _parse_docstring(fh): """Parse the docstrings of a script to find marked dependencies.""" find_fades = re.compile(r'\b(fades)\b:').search for line in fh: if line.startswith("'"): quote = "'" break if line.startswith('"'): quote = '"' break else: return {} if line[1] == quote: # comment start with triple quotes endquote = quote * 3 else: endquote = quote if endquote in line[len(endquote):]: docstring_lines = [line[:line.index(endquote)]] else: docstring_lines = [line] for line in fh: if endquote in line: docstring_lines.append(line[:line.index(endquote)]) break docstring_lines.append(line) docstring_lines = iter(docstring_lines) for doc_line in docstring_lines: if find_fades(doc_line): break else: return {} return _parse_requirement(list(docstring_lines)) def _parse_requirement(iterable): """Actually parse the requirements, from file or manually specified.""" deps = {} for line in iterable: line = line.strip() if "#" in line: line = line[:line.index("#")] if not line: continue parsed_req = parse_fade_requirement(line) if parsed_req is None: continue repo, dependency = parsed_req deps.setdefault(repo, []).append(dependency) return deps def parse_manual(dependencies): """Parse an iterable and return specified dependencies.""" if dependencies is None: return {} return _parse_requirement(dependencies) def _read_lines(filepath): """Read a req file to a list to support nested requirement files.""" with open(filepath, 'rt', encoding='utf8') as fh: for line in fh: line = line.strip() if line.startswith("-r"): logger.debug("Reading deps from nested requirement file: %s", line) try: nested_filename = line.split()[1] except IndexError: logger.warning( "Invalid format to indicate a nested requirements file: '%r'", line) else: nested_filepath = os.path.join( os.path.dirname(filepath), nested_filename) yield from _read_lines(nested_filepath) else: yield line def parse_reqfile(filepath): """Parse a requirement file and return the indicated dependencies.""" if filepath is None: return {} return _parse_requirement(_read_lines(filepath)) def parse_srcfile(filepath): """Parse a source file and return its marked dependencies.""" if filepath is None: return {} with open(filepath, 'rt', encoding='utf8') as fh: return _parse_content(fh) def parse_docstring(filepath): """Parse a source file and return its dependencies specified into docstrings.""" if filepath is None: return {} with open(filepath, 'rt', encoding='utf8') as fh: return _parse_docstring(fh) ================================================ FILE: fades/pipmanager.py ================================================ # Copyright 2014-2020 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Interface to handle pip. We are not using pip as a API because fades is not running in the same env that the child program. So we have to call the pip binary that is inside the created virtualenv. """ import os import logging import shutil import contextlib from urllib import request from fades import helpers logger = logging.getLogger(__name__) PIP_INSTALLER = "https://bootstrap.pypa.io/get-pip.py" class PipManager(): """A manager for all PIP related actions.""" def __init__(self, env_bin_path, pip_installed=False, options=None, avoid_pip_upgrade=False): """Init.""" self.env_bin_path = env_bin_path self.pip_installed = pip_installed self.options = options self.pip_exe = os.path.join(self.env_bin_path, "pip") basedir = helpers.get_basedir() self.pip_installer_fname = os.path.join(basedir, "get-pip.py") self.avoid_pip_upgrade = avoid_pip_upgrade def install(self, dependency): """Install a new dependency.""" if not self.pip_installed: logger.info("Need to install a dependency with pip, but no builtin, " "doing it manually (just wait a little, all should go well)") self._brute_force_install_pip() # Always update pip to get latest behaviours (specially regarding security); this has # the nice side effect of getting logged the pip version that is used. if not self.avoid_pip_upgrade: python_exe = os.path.join(self.env_bin_path, "python") helpers.logged_exec([python_exe, '-m', 'pip', 'install', 'pip', '--upgrade']) # split to pass several tokens on multiword dependency (this is very specific for '-e' on # external requirements, but implemented generically; note that this does not apply for # normal reqs, because even if it originally is 'foo > 1.2', after parsing it loses the # internal spaces) str_dep = str(dependency) args = [self.pip_exe, "install"] + str_dep.split() if self.options: for option in self.options: args.extend(option.split()) logger.info("Installing dependency: %r", str_dep) try: helpers.logged_exec(args) except helpers.ExecutionError as error: error.dump_to_log(logger) raise error except Exception as error: logger.exception("Error installing %s: %s", str_dep, error) raise error def get_version(self, dependency): """Return the installed version parsing the output of 'pip show'.""" logger.debug("getting installed version for %s", dependency) stdout = helpers.logged_exec([self.pip_exe, "show", str(dependency)]) version = [line for line in stdout if line.startswith('Version:')] if len(version) == 1: version = version[0].strip().split()[1] logger.debug("Installed version of %s is: %s", dependency, version) return version else: logger.error('Fades is having problems getting the installed version. ' 'Run with -v or check the logs for details') return '' def _download_pip_installer(self): u = request.urlopen(PIP_INSTALLER) temp_location = self.pip_installer_fname + '.temp' with contextlib.closing(u), open(temp_location, 'wb') as f: shutil.copyfileobj(u, f) os.rename(temp_location, self.pip_installer_fname) def _brute_force_install_pip(self): """Check a brute force install of pip itself.""" if os.path.exists(self.pip_installer_fname): logger.debug("Using pip installer from %r", self.pip_installer_fname) else: logger.debug( "Installer for pip not found in %r, downloading it", self.pip_installer_fname) self._download_pip_installer() logger.debug("Installing PIP manually in the virtualenv") python_exe = os.path.join(self.env_bin_path, "python") helpers.logged_exec([python_exe, self.pip_installer_fname, '-I']) self.pip_installed = True def freeze(self, filepath): """Dump venv contents to the indicated filepath.""" logger.debug("running freeze to store in %r", filepath) stdout = helpers.logged_exec([self.pip_exe, "freeze", "--all", "--local"]) with open(filepath, "wt", encoding='utf8') as fh: fh.writelines(line + '\n' for line in sorted(stdout)) ================================================ FILE: fades/pkgnamesdb.py ================================================ # Copyright 2015-2020 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """A module to package and viceversa conversion DB. This is needed for names which don't match with the distrbution's name. """ MODULE_TO_PACKAGE = { 'bs4': 'beautifulsoup4', 'github3': 'github3.py', 'uritemplate': 'uritemplate.py', 'postgresql': 'py-postgresql', 'yaml': 'pyyaml', 'PIL': 'pillow', 'Crypto': 'pycrypto', } PACKAGE_TO_MODULE = {v: k for k, v in MODULE_TO_PACKAGE.items()} ================================================ FILE: man/fades.1 ================================================ .TH FADES 1 .SH NAME fades - A system that automatically handles the virtualenvs in the cases normally found when writing scripts and simple programs, and even helps to administer big projects. .SH SYNOPSIS .B fades [\fB-h\fR][\fB--help\fR] [\fB-V\fR][\fB--version\fR] [\fB-v\fR][\fB--verbose\fR] [\fB-q\fR][\fB--quiet\fR] [\fB-i\fR][\fB--ipython\fR] [\fB-d\fR][\fB--dependency\fR] [\fB-r\fR][\fB--requirement\fR] [\fB-x\fR][\fB--exec\fR] [\fB-p\fR \fIversion\fR][\fB--python\fR=\fIversion\fR] [\fB--rm\fR=\fIUUID\fR] [\fB--system-site-packages\fR] [\fB--venv-options\fR=\fIoptions\fR] [\fB--pip-options\fR=\fIoptions\fR] [\fB--python-options\fR=\fIoptions\fR] [\fB-U\fR][\fB--check-updates\fR] [\fB--clean-unused-venvs\fR=\fImax_days_to_keep\fR] [\fB--where\fR][\fB--get-venv-dir\fR] [\fB--no-precheck-availability\fR] [\fB-a\fR][\fB--autoimport\fR] [\fB--freeze\fR] [\fB-m\fR][\fB--module\fR] [\fB--avoid-pip-upgrade\fR] [child_program [child_options]] \fBfades\fR can be used to execute directly your script, or put it with a #! at your script's beginning. .SH DESCRIPTION \fBfades\fR will automagically create a new virtual environment (or reuse a previous created one), installing the necessary dependencies, and execute your script inside that virtual environment, with the only requirement of executing the script with \fBfades\fR and also marking the required dependencies. The first non-option parameter (if any) would be then the child program to execute, and any other parameters after that are passed as is to that child script. \fBfades\fR can also be executed without passing a child script to execute: in this mode it will open a Python interactive interpreter inside the created/reused virtual environment (taking dependencies from \fI--dependency\fR or \fI--requirement\fR options). If \fI--autoimport\fR is given, it will automatically import all the installed dependencies. If the \fIchild_program\fR parameter is really an URL, the script will be automatically downloaded from there (supporting also the most common pastebins URLs: pastebin.com, linkode.org, gist, etc.). .SH OPTIONS .TP .BR -h ", "--help Show help about all the parameters and options, and quit. .TP .BR -V ", "--version Show the program version and info about the system, and quit. .TP .BR -v ", "--verbose Send all internal debugging lines to stderr, which may be very useful if any problem arises. .TP .BR -q ", " --quiet Don't show anything (unless it has a real problem), so the original script stderr is not polluted at all. .TP .BR -i ", " --ipython Runs IPython shell instead of python ones. .TP .BR -d ", " --dependency Specify dependencies through command line. This option can be specified multiple times (once per dependency), and each time the format is \fBrepository::dependency\fR. The dependency may have versions specifications, and the repository is optional (it will infer it). Examples: requests pypi::requests > 2.3 requests<=3 git+https://github.com/kennethreitz/requests.git#egg=requests vcs::git+https://github.com/kennethreitz/requests.git#egg=requests See more examples below for real command line usage explanations. .TP .BR -r ", " --requirement Read the dependencies from a file. Format in each line is the same than dependencies specified with \fI--dependency\fR. This option can be specified multiple times. .TP .BR -p " " \fIversion\fR ", " --python=\fIversion\fR Select which Python version to be used; the argument can be just the number (3.9), the whole name (python3.9) or the whole path (/usr/bin/python3.9). Of course, the corresponding version of Python needs to be installed in your system. The dependencies can be indicated in multiple places (in the Python source file, with a comment besides the import, in a \fIrequirements\fRfile, and/or through command line. In case of multiple definitions of the same dependency, command line overrides everything else, and requirements file overrides what is specified in the source code. .TP .BR -x ", " --exec Execute the \fIchild_program\fR in the context of the virtual environment. The child_program, which in this case becomes a mandatory parameter, can be just the executable name (relative to the venv's bin directory) or an absolute path. .TP .BR --rm " " \fIUUID\fR Remove a virtual environment by UUID. See \fB--get-venv-dir\fR option to easily find out the UUID. .TP .BR --system-site-packages "" Give the virtual environment access to thesystem site-packages dir .TP .BR --venv-options=\fIVIRTUALENV_OPTION\fR Extra options to be supplied to the venv module (this option can be used multiple times) .TP .BR --pip-options=\fIPIP_OPTION\fR Extra options to be supplied to pip. (this option can be used multiple times) .TP .BR --python-options=\fIPYTHON_OPTION\fR Extra options to be supplied to python. (this option can be used multiple times) .TP .BR -U ", " --check-updates Will check for updates in PyPI to verify if there are new versions for the requested dependencies. If a new version is available for a dependency, it will use it (if the dependency was requested without version) or just inform which new version is available (if the dependency was requested with a specific version). .TP .BR --clean-unused-venvs=\fIMAX_DAYS_TO_KEEP\fR Will remove all virtualenvs that haven't been used for more than MAX_DAYS_TO_KEEP days. .TP .BR --where ", " --get-venv-dir Show the virtual environment base directory (which includes the virtual environment UUID) and quit. .TP .BR --no-precheck-availability Don't check if the packages exists in PyPI before actually try to install them. .TP .BR -a ", " --autoimport Automatically import the dependencies when in interactive interpreter mode (ignored otherwise). .TP .BR --freeze " " \fIFILEPATH\fR Will operate exactly as without the command, but also it will dump the revisions of installed dependencies to the given \fBfilepath\fR. .TP .BR -m ", " --module Run library module as a script (same behaviour than Python's \fB-m\fR parameter). .TP .BR --avoid-pip-upgrade Disable the automatic \fBpip\fR upgrade that happens after the virtual environment is created and before the dependencies begin to be installed. .SH EXAMPLES .TP fades foo.py --bar Executes foo.py under fades, passing the --bar parameter to the child program, in a virtual environment with the dependencies indicated in the source code. .TP fades -v foo.py Executes foo.py under fades, showing all the fades messages (verbose mode). .TP fades -d dependency1 -d dependency2>3.2 foo.py --bar Executes foo.py under fades (passing the --bar parameter to it), in a virtual environment with the dependencies indicated in the source code and also dependency1 and dependency2 (any version > 3.2). .TP fades -d dependency1 Executes the Python interactive interpreter in a virtual environment with dependency1 installed. .TP fades -r requirements.txt Executes the Python interactive interpreter in a virtual environment after installing there all dependencies taken from the requirements.txt file. .TP fades -r requirements.txt -r requirements_devel.txt Executes the Python interactive interpreter in a virtual environment after installing there all dependencies taken from files requirements.txt and requirements_devel.txt. .SH USING CONFIGURATION FILES You can also configure fades using \fB.ini\fR config files. fades will search config files in \fB/etc/fades/fades.ini\fR, the path indicated by \fBxdg\fR for your system (for example ~/config/fades/fades.ini) and .fades.ini. So you can have different settings at system, user and project level. The config files are in .ini format. (configparser) and fades will search for a [fades] section. You have to use the same configurations that in the CLI. The only difference is with the config options with a dash, it has to be replaced with a underscore. Check http://fades.readthedocs.org/en/latest/readme.html#setting-options-using-config-files for full examples. .SH SEE ALSO Development is centralized in https://github.com/PyAr/fades Check that site for a better explanation of \fBfades\fR usage. .SH AUTHORS Facundo Batista, Nicolás Demarchi (see development page for contact info). .SH LICENSING This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. ================================================ FILE: pkg/debian/changelog ================================================ fades (4-1) unstable; urgency=medium * Initial release. (Closes: #802806) -- Facundo Batista Sun, 25 Oct 2015 12:30:40 -0300 ================================================ FILE: pkg/debian/compat ================================================ 9 ================================================ FILE: pkg/debian/control ================================================ Source: fades Section: python Priority: extra Build-Depends: debhelper (>= 9), dh-python, dh-translations | dh-python, python3-packaging, python3-all (>= 3.6), python3-xdg Maintainer: Facundo Batista Uploaders: Debian Python Modules Team Homepage: https://github.com/PyAr/fades Standards-Version: 3.9.7 X-Python3-Version: >= 3.6 Package: fades Architecture: all Depends: python3-pkg-resources, ${misc:Depends}, ${python3:Depends} Description: system for automatically handling virtual environments fades is a system that automatically handles the virtualenvs in the cases normally found when writing scripts and simple programs, and even helps to administer big projects. ================================================ FILE: pkg/debian/copyright ================================================ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: fades Upstream-Contact: Facundo Batista Source: https://github.com/PyAr/fades/ Files: * Copyright: (C) 2014-2024 Facundo Batista Nicolás Demarchi License: GPL-3 The full text of the GPL is distributed in /usr/share/common-licenses/GPL-3 on Debian systems. ================================================ FILE: pkg/debian/rules ================================================ #!/usr/bin/make -f export PYBUILD_NAME=fades # Debian doesn't have dh-translations. %: ifneq ($(shell dh -l | grep -xF translations),) dh $@ --with python3,translations --buildsystem=pybuild else dh $@ --with python3 --buildsystem=pybuild endif ================================================ FILE: pkg/debian/watch ================================================ version=3 opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ https://pypi.debian.net/fades/fades-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) ================================================ FILE: pkg/snap/snapcraft.yaml ================================================ name: fades summary: system for automatically handling virtual environments description: | fades is a system that automatically handles the virtualenvs in the cases normally found when writing scripts and simple programs, and even helps to administer big projects. fades will automagically create a new virtualenv (or reuse a previous created one), installing the necessary dependencies, and execute your script inside that virtualenv, with the only requirement of executing the script with fades and also marking the required dependencies. (If you don’t have a clue why this is necessary or useful, I'd recommend you to read this small text about Python and the Management of Dependencies: https://github.com/PyAr/fades/blob/master/docs/pydepmanag.rst) Check the full documentation here: https://fades.readthedocs.io/ For developers, the project is here: https://github.com/PyAr/fades icon: resources/logo256.png base: core20 confinement: classic grade: stable adopt-info: fades # look for 'snapcraftctl set-*' in the fades part apps: fades: command: bin/python3 -m fades parts: # Classic core20 snaps require staged python. python3: plugin: nil build-packages: - python3-dev stage-packages: - libpython3-stdlib - libpython3.8-minimal - libpython3.8-stdlib - python3.8-minimal - python3-distutils - python3-minimal - python3-pip - python3-packaging - python3-venv - python3-wheel fades: after: [python3] source: . plugin: python override-pull: | snapcraftctl pull snapcraftctl set-version "$( python3 -c 'import fades; print(fades._version.__version__)' )" override-build: | snapcraftctl build # python3 fixup symlink (snapcraft bug) ln -sf ../usr/bin/python3.8 $SNAPCRAFT_PART_INSTALL/bin/python3 ================================================ FILE: press.txt ================================================ Hello all, We're glad to announce the release of fades 9.0. fades is a system that automatically handles the virtualenvs in the cases normally found when writing scripts and simple programs, and even helps to administer big projects. It will automagically create a new virtualenv (or reuse a previous created one), installing the necessary dependencies, and execute your script inside that virtualenv. You only need to execute the script with fades (instead of Python) and also mark the required dependencies. More details here: http://fades.rtfd.org/ What's new in this release? - Get pip automatically upgraded to latest version on each virtualenv creation (unless explicitly avoided) - Provides the --freeze parameter, which dumps the detailed package information of the virtualenv, to duplicate future installations. - The -x/--exec parameter behaviour is extended/normalized to support arbitrary paths. - Has the --autoimport parameter to automatically import the dependencies to the interactive interpreter - Added more examples and descriptions to the documentation - Improved argument parsing when fades is used in the shebang - Worked on infrastructure: better testing, multiplatform installation support, etc. Nicolás and I want to say a big thank you to the following collaborators that helped to improve and enhance fades in different ways for this version (in alphabetical order): Alejandro Dau -https://github.com/alejandrodau Carlos Joel -https://github.com/c0x6a Diego Mascialino - https://github.com/dmascialino Eduardo Enriquez - https://github.com/eduzen Iñaki Malerba - https://github.com/inakimalerba To install and enjoy fades... - If you are in Ubuntu or Debian, you can easily install like this (but probably won't get *latest* fades: sudo apt-get install fades - For not latest debian/ubuntu you have a .deb here (with its Debian source file): http://ftp.debian.org/debian/pool/main/f/fades/fades_9.0.1-1_all.deb http://ftp.debian.org/debian/pool/main/f/fades/fades_9.0.1-1.dsc - Install it in Arch is very simple: yaourt -S fades - In any Linux if you have the Snap system: snap install fades - Using pip if you want: pip3 install fades - You can always get the multiplatform tarball and install it in the old fashion way: wget http://ftp.debian.org/debian/pool/main/f/fades/fades_9.0.1.orig.tar.gz tar -xf fades_*.tar.gz cd fades-* sudo ./setup.py install Help / questions: - You can ask any question or send any recommendation or request in the Telegram group: https://t.me/fadesmagic - Also, you can open an issue here (please do if you find any problem!). https://github.com/PyAr/fades/issues/new - The project itself is in https://github.com/PyAr/fades It's very easy to run latest development version: git clone https://github.com/PyAr/fades.git cd fades bin/fades Thanks in advance for your time! ---- Hola a todas y todos, Estamos encantados de anunciar la liberación de fades 9.0. fades es un sistema que maneja automáticamente los virtualenvs en los casos que uno normalmente encuentra al escribir scripts y programas pequeños, e incluso ayuda a administrar proyectos grandes. Crea automáticamente un nuevo virtualenv (o reusa uno creado previamente) instalando las dependencias necesarias, y ejecutando el script dentro de ese virtualenv. Todo lo que necesitás hacer es ejecutar el script con fades (en lugar de Python) y también marcar las dependencias necesarias. Más detalles en la [documentación del proyecto](http://fades.rtfd.org/). **¿Qué hay de nuevo en esta release?** - Hace que pip se actualice automáticamente a la última versión en la creación del virtualenv (a menos que se indique lo contrario). - Provee la opción `--freeze`, que graba la info detallada de los paquetes del virtualenv, para duplicar instalaciones futuras. - Extiende y normaliza el comportamiento del parámetro `-x/--exec` para soportar paths arbitrarios. - Crea la opción `--autoimport` para importar automáticamente las dependencias instaladas al entrar al intérprete interactivo. - Agrega ejemplos y descripciones a la documentación. - Mejora el parseo de argumentos cuando fades se usa en el shebang. - Se mejoró la infrastructura: mejores pruebas, soporte multiplataforma, etc. Nicolás y yo queremos darles muchas gracias a los siguientes colaboradores que ayudaron a mejorar a fades de distintas maneras para esta versión (en orden alfabético): - [Alejandro Dau](https://github.com/alejandrodau ) - [Carlos Joel](https://github.com/c0x6a) - [Diego Mascialino](https://github.com/dmascialino) - [Eduardo Enriquez](https://github.com/eduzen) - [Iñaki Malerba](https://github.com/inakimalerba) **Para instalar y disfrutar fades:** - Si estás en Ubuntu o Debian, podés facilmente instalarlo así (aunque probablemente no obtengas la *última* versión: `sudo apt-get install fades` - Para debian/ubuntu que no sea lo último, acá hay [un .deb](http://ftp.debian.org/debian/pool/main/f/fades/fades_9.0.1-1_all.deb) (con su respectivo [archivo fuente Debian](http://ftp.debian.org/debian/pool/main/f/fades/fades_9.0.1-1.dsc)). - Instalarlo en Arch es muy simple: `yaourt -S fades` - En cualquier Linux si tenés el sistema Snap: `snap install fades` - Podés usar pip si querés: `pip3 install fades` - Siempre podés usar el tarball multiplataforma e instalarlo de la manera clásica: ```bash wget http://ftp.debian.org/debian/pool/main/f/fades/fades_9.0.1.orig.tar.gz tar -xf fades_*.tar.gz cd fades-* sudo ./setup.py install ``` - Es muy fácil ejecutar la última versión de desarrollo:: ```bash git clone https://github.com/PyAr/fades.git cd fades bin/fades ``` **Ayuda / preguntas:** - Podés hacer cualquier pregunta o mandar una recomendación o pedido en [el grupo de Telegram](https://t.me/fadesmagic). - También podés [abrir un issue](https://github.com/PyAr/fades/issues/new) (por favor hacelo si encontrás algún problema!):: - El proyecto en sí está en [Github](https://github.com/PyAr/fades). Desde ya, muchas gracias por tu tiempo! ================================================ FILE: requirements.txt ================================================ flake8 logassert packaging pydocstyle pytest pyuca pyxdg rst2html5 ================================================ FILE: resources/gifs/gifs.rst ================================================ Several small gifs showing specific fades functionality ------------------------------------------------------- How to cleanly test a Python library from PyPI without having to install or create anything: .. image:: testpylibnoinstall.gif How to use a specific version of a Python library without messing your system: .. image:: usespecificlibversion.gif How to cleanly test a Python library from GitHub without having to install or create anything: .. image:: testpylibgithub.gif How to cleanly test a Python library without having to install or create anything and using `ipython`: .. image:: testlibwithipython.gif How to cleanly test a Python 2 library from PyPI without having to install or create anything: .. image:: testlibpylegacy.gif How to cleanly test multiple Python libraries without having to install or create anything: .. image:: multiplelibs.gif How to get a Python interpreter using some specific requirements: .. image:: usingreqs.gif How to make a Python script to be autonomous, not needing to remember any virtualenv details: .. image:: autonomousscript.gif How to create a Python web project using whatever Django specific version: .. image:: createdjangoproject.gif How to prepare your Python project for anybody to run it or its tests without any previous setup: .. image:: projecttests.gif How to open an isolated Jupyter notebook for Python with `pandas`, `matplotlib` and `numpy`: .. image:: jupyternotebook.gif How to run a modern timeit on a code snippet: .. image:: moderntimeit.gif How to use a library from a local branch, but isolated from the system: .. image:: locallib.gif How to automatically use the latest code to download YouTube videos: .. image:: youtubedl.gif ================================================ FILE: resources/notes.txt ================================================ wmctrl -r :ACTIVE: -e 0,424,52,880,495 880 x 482 !! kazam ================================================ FILE: setup.py ================================================ #!/usr/bin/env python3 # Copyright 2014-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Build tar.gz for fades. Needed packages to run (using Debian/Ubuntu package names): python3 python3-xdg (optional) """ import os import re import shutil import sys from distutils.core import setup from setuptools.command.install import install def get_version(): """Retrieves package version from the file.""" with open('fades/_version.py') as fh: m = re.search(r"\(([^']*)\)", fh.read()) if m is None: raise ValueError("Unrecognized version in 'fades/_version.py'") return m.groups()[0].replace(', ', '.') # the different scripts according to the platform SCRIPT_WIN = 'bin/fades.cmd' SCRIPT_REST = 'bin/fades' class CustomInstall(install): """Custom installation to fix script info and install man.""" def initialize_options(self): """Run parent initialization and then fix the scripts var.""" install.initialize_options(self) # leave the proper script according to the platform script = SCRIPT_WIN if sys.platform == "win32" else SCRIPT_REST self.distribution.scripts = [script] def run(self): """Run parent install, and then save the man file.""" install.run(self) # man directory if self._custom_man_dir is not None: if not os.path.exists(self._custom_man_dir): os.makedirs(self._custom_man_dir) shutil.copy("man/fades.1", self._custom_man_dir) def finalize_options(self): """Alter the installation path.""" install.finalize_options(self) if self.prefix is None: # no place for man page (like in a 'snap') man_dir = None else: man_dir = os.path.join(self.prefix, "share", "man", "man1") # if we have 'root', put the building path also under it (used normally # by pbuilder) if self.root is not None: man_dir = os.path.join(self.root, man_dir[1:]) self._custom_man_dir = man_dir setup( name='fades', version=get_version(), license='GPL-3', author='Facundo Batista, Nicolás Demarchi', author_email='facundo@taniquetil.com.ar, mail@gilgamezh.me', description=( 'A system that automatically handles the virtualenvs in the cases ' 'normally found when writing scripts and simple programs, and ' 'even helps to administer big projects.'), long_description=open('README.rst').read(), url='https://github.com/PyAr/fades', download_url="https://github.com/PyAr/fades/releases", # Release download URL. packages=["fades"], scripts=[SCRIPT_WIN, SCRIPT_REST], keywords="virtualenv utils utility scripts", # to get found easily on PyPI results, etc. cmdclass={ 'install': CustomInstall, }, install_requires=['setuptools'], tests_require=['logassert', 'pyxdg', 'pyuca', 'pytest', 'flake8', 'pep257', 'rst2html5'], # what unittests require python_requires='>=3.6', # Minimum Python version supported. extras_require={ 'pyxdg': 'pyxdg', 'packaging': 'packaging', }, # https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ 'Development Status :: 5 - Production/Stable', # 'Development Status :: 6 - Mature', # 'Development Status :: 7 - Inactive', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License (GPL)', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'Natural Language :: English', 'Natural Language :: Spanish', 'Operating System :: MacOS', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Software Development', 'Topic :: Utilities', ], ) ================================================ FILE: test ================================================ #!/bin/bash # # Copyright 2014-2018 Facundo Batista, Nicolás Demarchi set -eu if [ $# -ne 0 ]; then TARGET_TESTS="$@" else TARGET_TESTS="" fi ./bin/fades -r requirements.txt -d pytest-cov -x pytest --cov=fades $TARGET_TESTS # check if we are using exit() in the code. if grep -r -n ' exit(' --include="*.py" .; then echo 'Please use sys.exit() instead of exit(). https://github.com/PyAr/fades/issues/280'; fi ================================================ FILE: testdev ================================================ #!/bin/bash # # Copyright 2014-2016 Facundo Batista, Nicolás Demarchi set -eu if [ $# -ne 0 ]; then TARGET_TESTS="$@" else TARGET_TESTS="" fi ./bin/fades -r requirements.txt -x pytest -s $TARGET_TESTS ================================================ FILE: testdev.bat ================================================ @echo off rem Copyright 2018 Facundo Batista, Nicolás Demarchi if not [%*] == [] ( set TARGET_TESTS="%*" ) else ( set TARGET_TESTS=fades tests ) bin\fades -r requirements.txt -x pytest -v %TARGET_TESTS% ================================================ FILE: tests/__init__.py ================================================ # Copyright 2017-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Common code for the tests.""" import os from tempfile import mkstemp from packaging.requirements import Requirement def get_tempfile(testcase): """Return the name of a temp file that will be removed when the test finishes.""" # create the file and close its descriptor descriptor, tempfile = mkstemp(prefix="test-temp-file") os.close(descriptor) def clean(): """Clean the file from disk, if still there.""" if os.path.exists(tempfile): os.remove(tempfile) testcase.addCleanup(clean) return tempfile def create_tempfile(testcase, lines): tempfile = get_tempfile(testcase) with open(tempfile, 'w', encoding='utf-8') as f: for line in lines: f.write(line + '\n') return tempfile def get_python_filepaths(roots): """Helper to retrieve paths of Python files.""" python_paths = [] for root in roots: for dirpath, dirnames, filenames in os.walk(root): for filename in filenames: if filename.endswith(".py"): python_paths.append(os.path.join(dirpath, filename)) return python_paths def get_reqs(*items): """Transform text requirements into Requirement objects.""" return [Requirement(item) for item in items] ================================================ FILE: tests/conftest.py ================================================ # Copyright 2019-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades import uuid from pytest import fixture @fixture(scope="function") def tmp_file(tmp_path): """Fixture for a unique tmpfile for each test.""" yield str(tmp_path / "testfile") # XXX Facundo 2024-04-17: remove str() after #435 @fixture(scope="function") def create_tmpfile(tmp_path): def add_content(lines): """Fixture for a unique tmpfile for each test.""" namefile = tmp_path / f"testfile_{uuid.uuid4()}" with open(namefile, "w", encoding="utf-8") as f: for line in lines: f.write(line + "\n") return namefile yield add_content def pytest_addoption(parser): """Define new pytest command line argument to be used by integration tests.""" parser.addoption("--integtest-pyversion", action="store") ================================================ FILE: tests/examples/pypi_get_version_fail.json ================================================ {"MALFORMED": {"json": "1.0"}} ================================================ FILE: tests/examples/pypi_get_version_ok.json ================================================ { "info": { "maintainer": null, "docs_url": null, "requires_python": null, "maintainer_email": null, "cheesecake_code_kwalitee_id": null, "keywords": null, "package_url": "http://pypi.python.org/pypi/requests", "author": "Kenneth Reitz", "author_email": "me@kennethreitz.com", "download_url": "UNKNOWN", "platform": "UNKNOWN", "version": "2.8.1", "cheesecake_documentation_id": null, "_pypi_hidden": false, "description": "Requests: HTTP for Humans\n=========================\n\n.. image:: https://img.shields.io/pypi/v/requests.svg\n :target: https://pypi.python.org/pypi/requests\n\n.. image:: https://img.shields.io/pypi/dm/requests.svg\n :target: https://pypi.python.org/pypi/requests\n\n\n\n\nRequests is an Apache2 Licensed HTTP library, written in Python, for human\nbeings.\n\nMost existing Python modules for sending HTTP requests are extremely\nverbose and cumbersome. Python's builtin urllib2 module provides most of\nthe HTTP capabilities you should need, but the api is thoroughly broken.\nIt requires an enormous amount of work (even method overrides) to\nperform the simplest of tasks.\n\nThings shouldn't be this way. Not in Python.\n\n.. code-block:: python\n\n >>> r = requests.get('https://api.github.com', auth=('user', 'pass'))\n >>> r.status_code\n 204\n >>> r.headers['content-type']\n 'application/json'\n >>> r.text\n ...\n\nSee `the same code, without Requests `_.\n\nRequests allow you to send HTTP/1.1 requests. You can add headers, form data,\nmultipart files, and parameters with simple Python dictionaries, and access the\nresponse data in the same way. It's powered by httplib and `urllib3\n`_, but it does all the hard work and crazy\nhacks for you.\n\n\nFeatures\n--------\n\n- International Domains and URLs\n- Keep-Alive & Connection Pooling\n- Sessions with Cookie Persistence\n- Browser-style SSL Verification\n- Basic/Digest Authentication\n- Elegant Key/Value Cookies\n- Automatic Decompression\n- Unicode Response Bodies\n- Multipart File Uploads\n- Connection Timeouts\n- Thread-safety\n- HTTP(S) proxy support\n\n\nInstallation\n------------\n\nTo install Requests, simply:\n\n.. code-block:: bash\n\n $ pip install requests\n\n\nDocumentation\n-------------\n\nDocumentation is available at http://docs.python-requests.org/.\n\n\nContribute\n----------\n\n#. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. There is a `Contributor Friendly`_ tag for issues that should be ideal for people who are not very familiar with the codebase yet.\n#. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it).\n#. Write a test which shows that the bug was fixed or that the feature works as expected.\n#. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS_.\n\n.. _`the repository`: http://github.com/kennethreitz/requests\n.. _AUTHORS: https://github.com/kennethreitz/requests/blob/master/AUTHORS.rst\n.. _Contributor Friendly: https://github.com/kennethreitz/requests/issues?direction=desc&labels=Contributor+Friendly&page=1&sort=updated&state=open\n\n\n.. :changelog:\n\nRelease History\n---------------\n\n2.8.1 (2015-10-13)\n++++++++++++++++++\n\n**Bugfixes**\n\n- Update certificate bundle to match ``certifi`` 2015.9.6.2's weak certificate\n bundle.\n- Fix a bug in 2.8.0 where requests would raise ``ConnectTimeout`` instead of\n ``ConnectionError``\n- When using the PreparedRequest flow, requests will now correctly respect the\n ``json`` parameter. Broken in 2.8.0.\n- When using the PreparedRequest flow, requests will now correctly handle a\n Unicode-string method name on Python 2. Broken in 2.8.0.\n\n2.8.0 (2015-10-05)\n++++++++++++++++++\n\n**Minor Improvements** (Backwards Compatible)\n\n- Requests now supports per-host proxies. This allows the ``proxies``\n dictionary to have entries of the form\n ``{'://': ''}``. Host-specific proxies will be used\n in preference to the previously-supported scheme-specific ones, but the\n previous syntax will continue to work.\n- ``Response.raise_for_status`` now prints the URL that failed as part of the\n exception message.\n- ``requests.utils.get_netrc_auth`` now takes an ``raise_errors`` kwarg,\n defaulting to ``False``. When ``True``, errors parsing ``.netrc`` files cause\n exceptions to be thrown.\n- Change to bundled projects import logic to make it easier to unbundle\n requests downstream.\n- Changed the default User-Agent string to avoid leaking data on Linux: now\n contains only the requests version.\n\n**Bugfixes**\n\n- The ``json`` parameter to ``post()`` and friends will now only be used if\n neither ``data`` nor ``files`` are present, consistent with the\n documentation.\n- We now ignore empty fields in the ``NO_PROXY`` enviroment variable.\n- Fixed problem where ``httplib.BadStatusLine`` would get raised if combining\n ``stream=True`` with ``contextlib.closing``.\n- Prevented bugs where we would attempt to return the same connection back to\n the connection pool twice when sending a Chunked body.\n- Miscellaneous minor internal changes.\n- Digest Auth support is now thread safe.\n\n**Updates**\n\n- Updated urllib3 to 1.12.\n\n2.7.0 (2015-05-03)\n++++++++++++++++++\n\nThis is the first release that follows our new release process. For more, see\n`our documentation\n`_.\n\n**Bugfixes**\n\n- Updated urllib3 to 1.10.4, resolving several bugs involving chunked transfer\n encoding and response framing.\n\n2.6.2 (2015-04-23)\n++++++++++++++++++\n\n**Bugfixes**\n\n- Fix regression where compressed data that was sent as chunked data was not\n properly decompressed. (#2561)\n\n2.6.1 (2015-04-22)\n++++++++++++++++++\n\n**Bugfixes**\n\n- Remove VendorAlias import machinery introduced in v2.5.2.\n\n- Simplify the PreparedRequest.prepare API: We no longer require the user to\n pass an empty list to the hooks keyword argument. (c.f. #2552)\n\n- Resolve redirects now receives and forwards all of the original arguments to\n the adapter. (#2503)\n\n- Handle UnicodeDecodeErrors when trying to deal with a unicode URL that\n cannot be encoded in ASCII. (#2540)\n\n- Populate the parsed path of the URI field when performing Digest\n Authentication. (#2426)\n\n- Copy a PreparedRequest's CookieJar more reliably when it is not an instance\n of RequestsCookieJar. (#2527)\n\n2.6.0 (2015-03-14)\n++++++++++++++++++\n\n**Bugfixes**\n\n- CVE-2015-2296: Fix handling of cookies on redirect. Previously a cookie\n without a host value set would use the hostname for the redirected URL\n exposing requests users to session fixation attacks and potentially cookie\n stealing. This was disclosed privately by Matthew Daley of\n `BugFuzz `_. This affects all versions of requests from\n v2.1.0 to v2.5.3 (inclusive on both ends).\n\n- Fix error when requests is an ``install_requires`` dependency and ``python\n setup.py test`` is run. (#2462)\n\n- Fix error when urllib3 is unbundled and requests continues to use the\n vendored import location.\n\n- Include fixes to ``urllib3``'s header handling.\n\n- Requests' handling of unvendored dependencies is now more restrictive.\n\n**Features and Improvements**\n\n- Support bytearrays when passed as parameters in the ``files`` argument.\n (#2468)\n\n- Avoid data duplication when creating a request with ``str``, ``bytes``, or\n ``bytearray`` input to the ``files`` argument.\n\n2.5.3 (2015-02-24)\n++++++++++++++++++\n\n**Bugfixes**\n\n- Revert changes to our vendored certificate bundle. For more context see\n (#2455, #2456, and http://bugs.python.org/issue23476)\n\n2.5.2 (2015-02-23)\n++++++++++++++++++\n\n**Features and Improvements**\n\n- Add sha256 fingerprint support. (`shazow/urllib3#540`_)\n\n- Improve the performance of headers. (`shazow/urllib3#544`_)\n\n**Bugfixes**\n\n- Copy pip's import machinery. When downstream redistributors remove\n requests.packages.urllib3 the import machinery will continue to let those\n same symbols work. Example usage in requests' documentation and 3rd-party\n libraries relying on the vendored copies of urllib3 will work without having\n to fallback to the system urllib3.\n\n- Attempt to quote parts of the URL on redirect if unquoting and then quoting\n fails. (#2356)\n\n- Fix filename type check for multipart form-data uploads. (#2411)\n\n- Properly handle the case where a server issuing digest authentication\n challenges provides both auth and auth-int qop-values. (#2408)\n\n- Fix a socket leak. (`shazow/urllib3#549`_)\n\n- Fix multiple ``Set-Cookie`` headers properly. (`shazow/urllib3#534`_)\n\n- Disable the built-in hostname verification. (`shazow/urllib3#526`_)\n\n- Fix the behaviour of decoding an exhausted stream. (`shazow/urllib3#535`_)\n\n**Security**\n\n- Pulled in an updated ``cacert.pem``.\n\n- Drop RC4 from the default cipher list. (`shazow/urllib3#551`_)\n\n.. _shazow/urllib3#551: https://github.com/shazow/urllib3/pull/551\n.. _shazow/urllib3#549: https://github.com/shazow/urllib3/pull/549\n.. _shazow/urllib3#544: https://github.com/shazow/urllib3/pull/544\n.. _shazow/urllib3#540: https://github.com/shazow/urllib3/pull/540\n.. _shazow/urllib3#535: https://github.com/shazow/urllib3/pull/535\n.. _shazow/urllib3#534: https://github.com/shazow/urllib3/pull/534\n.. _shazow/urllib3#526: https://github.com/shazow/urllib3/pull/526\n\n2.5.1 (2014-12-23)\n++++++++++++++++++\n\n**Behavioural Changes**\n\n- Only catch HTTPErrors in raise_for_status (#2382)\n\n**Bugfixes**\n\n- Handle LocationParseError from urllib3 (#2344)\n- Handle file-like object filenames that are not strings (#2379)\n- Unbreak HTTPDigestAuth handler. Allow new nonces to be negotiated (#2389)\n\n2.5.0 (2014-12-01)\n++++++++++++++++++\n\n**Improvements**\n\n- Allow usage of urllib3's Retry object with HTTPAdapters (#2216)\n- The ``iter_lines`` method on a response now accepts a delimiter with which\n to split the content (#2295)\n\n**Behavioural Changes**\n\n- Add deprecation warnings to functions in requests.utils that will be removed\n in 3.0 (#2309)\n- Sessions used by the functional API are always closed (#2326)\n- Restrict requests to HTTP/1.1 and HTTP/1.0 (stop accepting HTTP/0.9) (#2323)\n\n**Bugfixes**\n\n- Only parse the URL once (#2353)\n- Allow Content-Length header to always be overridden (#2332)\n- Properly handle files in HTTPDigestAuth (#2333)\n- Cap redirect_cache size to prevent memory abuse (#2299)\n- Fix HTTPDigestAuth handling of redirects after authenticating successfully\n (#2253)\n- Fix crash with custom method parameter to Session.request (#2317)\n- Fix how Link headers are parsed using the regular expression library (#2271)\n\n**Documentation**\n\n- Add more references for interlinking (#2348)\n- Update CSS for theme (#2290)\n- Update width of buttons and sidebar (#2289)\n- Replace references of Gittip with Gratipay (#2282)\n- Add link to changelog in sidebar (#2273)\n\n2.4.3 (2014-10-06)\n++++++++++++++++++\n\n**Bugfixes**\n\n- Unicode URL improvements for Python 2.\n- Re-order JSON param for backwards compat.\n- Automatically defrag authentication schemes from host/pass URIs. (`#2249 `_)\n\n\n2.4.2 (2014-10-05)\n++++++++++++++++++\n\n**Improvements**\n\n- FINALLY! Add json parameter for uploads! (`#2258 `_)\n- Support for bytestring URLs on Python 3.x (`#2238 `_)\n\n**Bugfixes**\n\n- Avoid getting stuck in a loop (`#2244 `_)\n- Multiple calls to iter* fail with unhelpful error. (`#2240 `_, `#2241 `_)\n\n**Documentation**\n\n- Correct redirection introduction (`#2245 `_)\n- Added example of how to send multiple files in one request. (`#2227 `_)\n- Clarify how to pass a custom set of CAs (`#2248 `_)\n\n\n\n2.4.1 (2014-09-09)\n++++++++++++++++++\n\n- Now has a \"security\" package extras set, ``$ pip install requests[security]``\n- Requests will now use Certifi if it is available.\n- Capture and re-raise urllib3 ProtocolError\n- Bugfix for responses that attempt to redirect to themselves forever (wtf?).\n\n\n2.4.0 (2014-08-29)\n++++++++++++++++++\n\n**Behavioral Changes**\n\n- ``Connection: keep-alive`` header is now sent automatically.\n\n**Improvements**\n\n- Support for connect timeouts! Timeout now accepts a tuple (connect, read) which is used to set individual connect and read timeouts.\n- Allow copying of PreparedRequests without headers/cookies.\n- Updated bundled urllib3 version.\n- Refactored settings loading from environment -- new `Session.merge_environment_settings`.\n- Handle socket errors in iter_content.\n\n\n2.3.0 (2014-05-16)\n++++++++++++++++++\n\n**API Changes**\n\n- New ``Response`` property ``is_redirect``, which is true when the\n library could have processed this response as a redirection (whether\n or not it actually did).\n- The ``timeout`` parameter now affects requests with both ``stream=True`` and\n ``stream=False`` equally.\n- The change in v2.0.0 to mandate explicit proxy schemes has been reverted.\n Proxy schemes now default to ``http://``.\n- The ``CaseInsensitiveDict`` used for HTTP headers now behaves like a normal\n dictionary when references as string or viewed in the interpreter.\n\n**Bugfixes**\n\n- No longer expose Authorization or Proxy-Authorization headers on redirect.\n Fix CVE-2014-1829 and CVE-2014-1830 respectively.\n- Authorization is re-evaluated each redirect.\n- On redirect, pass url as native strings.\n- Fall-back to autodetected encoding for JSON when Unicode detection fails.\n- Headers set to ``None`` on the ``Session`` are now correctly not sent.\n- Correctly honor ``decode_unicode`` even if it wasn't used earlier in the same\n response.\n- Stop advertising ``compress`` as a supported Content-Encoding.\n- The ``Response.history`` parameter is now always a list.\n- Many, many ``urllib3`` bugfixes.\n\n2.2.1 (2014-01-23)\n++++++++++++++++++\n\n**Bugfixes**\n\n- Fixes incorrect parsing of proxy credentials that contain a literal or encoded '#' character.\n- Assorted urllib3 fixes.\n\n2.2.0 (2014-01-09)\n++++++++++++++++++\n\n**API Changes**\n\n- New exception: ``ContentDecodingError``. Raised instead of ``urllib3``\n ``DecodeError`` exceptions.\n\n**Bugfixes**\n\n- Avoid many many exceptions from the buggy implementation of ``proxy_bypass`` on OS X in Python 2.6.\n- Avoid crashing when attempting to get authentication credentials from ~/.netrc when running as a user without a home directory.\n- Use the correct pool size for pools of connections to proxies.\n- Fix iteration of ``CookieJar`` objects.\n- Ensure that cookies are persisted over redirect.\n- Switch back to using chardet, since it has merged with charade.\n\n2.1.0 (2013-12-05)\n++++++++++++++++++\n\n- Updated CA Bundle, of course.\n- Cookies set on individual Requests through a ``Session`` (e.g. via ``Session.get()``) are no longer persisted to the ``Session``.\n- Clean up connections when we hit problems during chunked upload, rather than leaking them.\n- Return connections to the pool when a chunked upload is successful, rather than leaking it.\n- Match the HTTPbis recommendation for HTTP 301 redirects.\n- Prevent hanging when using streaming uploads and Digest Auth when a 401 is received.\n- Values of headers set by Requests are now always the native string type.\n- Fix previously broken SNI support.\n- Fix accessing HTTP proxies using proxy authentication.\n- Unencode HTTP Basic usernames and passwords extracted from URLs.\n- Support for IP address ranges for no_proxy environment variable\n- Parse headers correctly when users override the default ``Host:`` header.\n- Avoid munging the URL in case of case-sensitive servers.\n- Looser URL handling for non-HTTP/HTTPS urls.\n- Accept unicode methods in Python 2.6 and 2.7.\n- More resilient cookie handling.\n- Make ``Response`` objects pickleable.\n- Actually added MD5-sess to Digest Auth instead of pretending to like last time.\n- Updated internal urllib3.\n- Fixed @Lukasa's lack of taste.\n\n2.0.1 (2013-10-24)\n++++++++++++++++++\n\n- Updated included CA Bundle with new mistrusts and automated process for the future\n- Added MD5-sess to Digest Auth\n- Accept per-file headers in multipart file POST messages.\n- Fixed: Don't send the full URL on CONNECT messages.\n- Fixed: Correctly lowercase a redirect scheme.\n- Fixed: Cookies not persisted when set via functional API.\n- Fixed: Translate urllib3 ProxyError into a requests ProxyError derived from ConnectionError.\n- Updated internal urllib3 and chardet.\n\n2.0.0 (2013-09-24)\n++++++++++++++++++\n\n**API Changes:**\n\n- Keys in the Headers dictionary are now native strings on all Python versions,\n i.e. bytestrings on Python 2, unicode on Python 3.\n- Proxy URLs now *must* have an explicit scheme. A ``MissingSchema`` exception\n will be raised if they don't.\n- Timeouts now apply to read time if ``Stream=False``.\n- ``RequestException`` is now a subclass of ``IOError``, not ``RuntimeError``.\n- Added new method to ``PreparedRequest`` objects: ``PreparedRequest.copy()``.\n- Added new method to ``Session`` objects: ``Session.update_request()``. This\n method updates a ``Request`` object with the data (e.g. cookies) stored on\n the ``Session``.\n- Added new method to ``Session`` objects: ``Session.prepare_request()``. This\n method updates and prepares a ``Request`` object, and returns the\n corresponding ``PreparedRequest`` object.\n- Added new method to ``HTTPAdapter`` objects: ``HTTPAdapter.proxy_headers()``.\n This should not be called directly, but improves the subclass interface.\n- ``httplib.IncompleteRead`` exceptions caused by incorrect chunked encoding\n will now raise a Requests ``ChunkedEncodingError`` instead.\n- Invalid percent-escape sequences now cause a Requests ``InvalidURL``\n exception to be raised.\n- HTTP 208 no longer uses reason phrase ``\"im_used\"``. Correctly uses\n ``\"already_reported\"``.\n- HTTP 226 reason added (``\"im_used\"``).\n\n**Bugfixes:**\n\n- Vastly improved proxy support, including the CONNECT verb. Special thanks to\n the many contributors who worked towards this improvement.\n- Cookies are now properly managed when 401 authentication responses are\n received.\n- Chunked encoding fixes.\n- Support for mixed case schemes.\n- Better handling of streaming downloads.\n- Retrieve environment proxies from more locations.\n- Minor cookies fixes.\n- Improved redirect behaviour.\n- Improved streaming behaviour, particularly for compressed data.\n- Miscellaneous small Python 3 text encoding bugs.\n- ``.netrc`` no longer overrides explicit auth.\n- Cookies set by hooks are now correctly persisted on Sessions.\n- Fix problem with cookies that specify port numbers in their host field.\n- ``BytesIO`` can be used to perform streaming uploads.\n- More generous parsing of the ``no_proxy`` environment variable.\n- Non-string objects can be passed in data values alongside files.\n\n1.2.3 (2013-05-25)\n++++++++++++++++++\n\n- Simple packaging fix\n\n\n1.2.2 (2013-05-23)\n++++++++++++++++++\n\n- Simple packaging fix\n\n\n1.2.1 (2013-05-20)\n++++++++++++++++++\n\n- 301 and 302 redirects now change the verb to GET for all verbs, not just\n POST, improving browser compatibility.\n- Python 3.3.2 compatibility\n- Always percent-encode location headers\n- Fix connection adapter matching to be most-specific first\n- new argument to the default connection adapter for passing a block argument\n- prevent a KeyError when there's no link headers\n\n1.2.0 (2013-03-31)\n++++++++++++++++++\n\n- Fixed cookies on sessions and on requests\n- Significantly change how hooks are dispatched - hooks now receive all the\n arguments specified by the user when making a request so hooks can make a\n secondary request with the same parameters. This is especially necessary for\n authentication handler authors\n- certifi support was removed\n- Fixed bug where using OAuth 1 with body ``signature_type`` sent no data\n- Major proxy work thanks to @Lukasa including parsing of proxy authentication\n from the proxy url\n- Fix DigestAuth handling too many 401s\n- Update vendored urllib3 to include SSL bug fixes\n- Allow keyword arguments to be passed to ``json.loads()`` via the\n ``Response.json()`` method\n- Don't send ``Content-Length`` header by default on ``GET`` or ``HEAD``\n requests\n- Add ``elapsed`` attribute to ``Response`` objects to time how long a request\n took.\n- Fix ``RequestsCookieJar``\n- Sessions and Adapters are now picklable, i.e., can be used with the\n multiprocessing library\n- Update charade to version 1.0.3\n\nThe change in how hooks are dispatched will likely cause a great deal of\nissues.\n\n1.1.0 (2013-01-10)\n++++++++++++++++++\n\n- CHUNKED REQUESTS\n- Support for iterable response bodies\n- Assume servers persist redirect params\n- Allow explicit content types to be specified for file data\n- Make merge_kwargs case-insensitive when looking up keys\n\n1.0.3 (2012-12-18)\n++++++++++++++++++\n\n- Fix file upload encoding bug\n- Fix cookie behavior\n\n1.0.2 (2012-12-17)\n++++++++++++++++++\n\n- Proxy fix for HTTPAdapter.\n\n1.0.1 (2012-12-17)\n++++++++++++++++++\n\n- Cert verification exception bug.\n- Proxy fix for HTTPAdapter.\n\n1.0.0 (2012-12-17)\n++++++++++++++++++\n\n- Massive Refactor and Simplification\n- Switch to Apache 2.0 license\n- Swappable Connection Adapters\n- Mountable Connection Adapters\n- Mutable ProcessedRequest chain\n- /s/prefetch/stream\n- Removal of all configuration\n- Standard library logging\n- Make Response.json() callable, not property.\n- Usage of new charade project, which provides python 2 and 3 simultaneous chardet.\n- Removal of all hooks except 'response'\n- Removal of all authentication helpers (OAuth, Kerberos)\n\nThis is not a backwards compatible change.\n\n0.14.2 (2012-10-27)\n+++++++++++++++++++\n\n- Improved mime-compatible JSON handling\n- Proxy fixes\n- Path hack fixes\n- Case-Insensistive Content-Encoding headers\n- Support for CJK parameters in form posts\n\n\n0.14.1 (2012-10-01)\n+++++++++++++++++++\n\n- Python 3.3 Compatibility\n- Simply default accept-encoding\n- Bugfixes\n\n\n0.14.0 (2012-09-02)\n++++++++++++++++++++\n\n- No more iter_content errors if already downloaded.\n\n0.13.9 (2012-08-25)\n+++++++++++++++++++\n\n- Fix for OAuth + POSTs\n- Remove exception eating from dispatch_hook\n- General bugfixes\n\n0.13.8 (2012-08-21)\n+++++++++++++++++++\n\n- Incredible Link header support :)\n\n0.13.7 (2012-08-19)\n+++++++++++++++++++\n\n- Support for (key, value) lists everywhere.\n- Digest Authentication improvements.\n- Ensure proxy exclusions work properly.\n- Clearer UnicodeError exceptions.\n- Automatic casting of URLs to strings (fURL and such)\n- Bugfixes.\n\n0.13.6 (2012-08-06)\n+++++++++++++++++++\n\n- Long awaited fix for hanging connections!\n\n0.13.5 (2012-07-27)\n+++++++++++++++++++\n\n- Packaging fix\n\n0.13.4 (2012-07-27)\n+++++++++++++++++++\n\n- GSSAPI/Kerberos authentication!\n- App Engine 2.7 Fixes!\n- Fix leaking connections (from urllib3 update)\n- OAuthlib path hack fix\n- OAuthlib URL parameters fix.\n\n0.13.3 (2012-07-12)\n+++++++++++++++++++\n\n- Use simplejson if available.\n- Do not hide SSLErrors behind Timeouts.\n- Fixed param handling with urls containing fragments.\n- Significantly improved information in User Agent.\n- client certificates are ignored when verify=False\n\n0.13.2 (2012-06-28)\n+++++++++++++++++++\n\n- Zero dependencies (once again)!\n- New: Response.reason\n- Sign querystring parameters in OAuth 1.0\n- Client certificates no longer ignored when verify=False\n- Add openSUSE certificate support\n\n0.13.1 (2012-06-07)\n+++++++++++++++++++\n\n- Allow passing a file or file-like object as data.\n- Allow hooks to return responses that indicate errors.\n- Fix Response.text and Response.json for body-less responses.\n\n0.13.0 (2012-05-29)\n+++++++++++++++++++\n\n- Removal of Requests.async in favor of `grequests `_\n- Allow disabling of cookie persistiance.\n- New implementation of safe_mode\n- cookies.get now supports default argument\n- Session cookies not saved when Session.request is called with return_response=False\n- Env: no_proxy support.\n- RequestsCookieJar improvements.\n- Various bug fixes.\n\n0.12.1 (2012-05-08)\n+++++++++++++++++++\n\n- New ``Response.json`` property.\n- Ability to add string file uploads.\n- Fix out-of-range issue with iter_lines.\n- Fix iter_content default size.\n- Fix POST redirects containing files.\n\n0.12.0 (2012-05-02)\n+++++++++++++++++++\n\n- EXPERIMENTAL OAUTH SUPPORT!\n- Proper CookieJar-backed cookies interface with awesome dict-like interface.\n- Speed fix for non-iterated content chunks.\n- Move ``pre_request`` to a more usable place.\n- New ``pre_send`` hook.\n- Lazily encode data, params, files.\n- Load system Certificate Bundle if ``certify`` isn't available.\n- Cleanups, fixes.\n\n0.11.2 (2012-04-22)\n+++++++++++++++++++\n\n- Attempt to use the OS's certificate bundle if ``certifi`` isn't available.\n- Infinite digest auth redirect fix.\n- Multi-part file upload improvements.\n- Fix decoding of invalid %encodings in URLs.\n- If there is no content in a response don't throw an error the second time that content is attempted to be read.\n- Upload data on redirects.\n\n0.11.1 (2012-03-30)\n+++++++++++++++++++\n\n* POST redirects now break RFC to do what browsers do: Follow up with a GET.\n* New ``strict_mode`` configuration to disable new redirect behavior.\n\n\n0.11.0 (2012-03-14)\n+++++++++++++++++++\n\n* Private SSL Certificate support\n* Remove select.poll from Gevent monkeypatching\n* Remove redundant generator for chunked transfer encoding\n* Fix: Response.ok raises Timeout Exception in safe_mode\n\n0.10.8 (2012-03-09)\n+++++++++++++++++++\n\n* Generate chunked ValueError fix\n* Proxy configuration by environment variables\n* Simplification of iter_lines.\n* New `trust_env` configuration for disabling system/environment hints.\n* Suppress cookie errors.\n\n0.10.7 (2012-03-07)\n+++++++++++++++++++\n\n* `encode_uri` = False\n\n0.10.6 (2012-02-25)\n+++++++++++++++++++\n\n* Allow '=' in cookies.\n\n0.10.5 (2012-02-25)\n+++++++++++++++++++\n\n* Response body with 0 content-length fix.\n* New async.imap.\n* Don't fail on netrc.\n\n\n0.10.4 (2012-02-20)\n+++++++++++++++++++\n\n* Honor netrc.\n\n0.10.3 (2012-02-20)\n+++++++++++++++++++\n\n* HEAD requests don't follow redirects anymore.\n* raise_for_status() doesn't raise for 3xx anymore.\n* Make Session objects picklable.\n* ValueError for invalid schema URLs.\n\n0.10.2 (2012-01-15)\n+++++++++++++++++++\n\n* Vastly improved URL quoting.\n* Additional allowed cookie key values.\n* Attempted fix for \"Too many open files\" Error\n* Replace unicode errors on first pass, no need for second pass.\n* Append '/' to bare-domain urls before query insertion.\n* Exceptions now inherit from RuntimeError.\n* Binary uploads + auth fix.\n* Bugfixes.\n\n\n0.10.1 (2012-01-23)\n+++++++++++++++++++\n\n* PYTHON 3 SUPPORT!\n* Dropped 2.5 Support. (*Backwards Incompatible*)\n\n0.10.0 (2012-01-21)\n+++++++++++++++++++\n\n* ``Response.content`` is now bytes-only. (*Backwards Incompatible*)\n* New ``Response.text`` is unicode-only.\n* If no ``Response.encoding`` is specified and ``chardet`` is available, ``Response.text`` will guess an encoding.\n* Default to ISO-8859-1 (Western) encoding for \"text\" subtypes.\n* Removal of `decode_unicode`. (*Backwards Incompatible*)\n* New multiple-hooks system.\n* New ``Response.register_hook`` for registering hooks within the pipeline.\n* ``Response.url`` is now Unicode.\n\n0.9.3 (2012-01-18)\n++++++++++++++++++\n\n* SSL verify=False bugfix (apparent on windows machines).\n\n0.9.2 (2012-01-18)\n++++++++++++++++++\n\n* Asynchronous async.send method.\n* Support for proper chunk streams with boundaries.\n* session argument for Session classes.\n* Print entire hook tracebacks, not just exception instance.\n* Fix response.iter_lines from pending next line.\n* Fix but in HTTP-digest auth w/ URI having query strings.\n* Fix in Event Hooks section.\n* Urllib3 update.\n\n\n0.9.1 (2012-01-06)\n++++++++++++++++++\n\n* danger_mode for automatic Response.raise_for_status()\n* Response.iter_lines refactor\n\n0.9.0 (2011-12-28)\n++++++++++++++++++\n\n* verify ssl is default.\n\n\n0.8.9 (2011-12-28)\n++++++++++++++++++\n\n* Packaging fix.\n\n\n0.8.8 (2011-12-28)\n++++++++++++++++++\n\n* SSL CERT VERIFICATION!\n* Release of Cerifi: Mozilla's cert list.\n* New 'verify' argument for SSL requests.\n* Urllib3 update.\n\n0.8.7 (2011-12-24)\n++++++++++++++++++\n\n* iter_lines last-line truncation fix\n* Force safe_mode for async requests\n* Handle safe_mode exceptions more consistently\n* Fix iteration on null responses in safe_mode\n\n0.8.6 (2011-12-18)\n++++++++++++++++++\n\n* Socket timeout fixes.\n* Proxy Authorization support.\n\n0.8.5 (2011-12-14)\n++++++++++++++++++\n\n* Response.iter_lines!\n\n0.8.4 (2011-12-11)\n++++++++++++++++++\n\n* Prefetch bugfix.\n* Added license to installed version.\n\n0.8.3 (2011-11-27)\n++++++++++++++++++\n\n* Converted auth system to use simpler callable objects.\n* New session parameter to API methods.\n* Display full URL while logging.\n\n0.8.2 (2011-11-19)\n++++++++++++++++++\n\n* New Unicode decoding system, based on over-ridable `Response.encoding`.\n* Proper URL slash-quote handling.\n* Cookies with ``[``, ``]``, and ``_`` allowed.\n\n0.8.1 (2011-11-15)\n++++++++++++++++++\n\n* URL Request path fix\n* Proxy fix.\n* Timeouts fix.\n\n0.8.0 (2011-11-13)\n++++++++++++++++++\n\n* Keep-alive support!\n* Complete removal of Urllib2\n* Complete removal of Poster\n* Complete removal of CookieJars\n* New ConnectionError raising\n* Safe_mode for error catching\n* prefetch parameter for request methods\n* OPTION method\n* Async pool size throttling\n* File uploads send real names\n* Vendored in urllib3\n\n0.7.6 (2011-11-07)\n++++++++++++++++++\n\n* Digest authentication bugfix (attach query data to path)\n\n0.7.5 (2011-11-04)\n++++++++++++++++++\n\n* Response.content = None if there was an invalid response.\n* Redirection auth handling.\n\n0.7.4 (2011-10-26)\n++++++++++++++++++\n\n* Session Hooks fix.\n\n0.7.3 (2011-10-23)\n++++++++++++++++++\n\n* Digest Auth fix.\n\n\n0.7.2 (2011-10-23)\n++++++++++++++++++\n\n* PATCH Fix.\n\n\n0.7.1 (2011-10-23)\n++++++++++++++++++\n\n* Move away from urllib2 authentication handling.\n* Fully Remove AuthManager, AuthObject, &c.\n* New tuple-based auth system with handler callbacks.\n\n\n0.7.0 (2011-10-22)\n++++++++++++++++++\n\n* Sessions are now the primary interface.\n* Deprecated InvalidMethodException.\n* PATCH fix.\n* New config system (no more global settings).\n\n\n0.6.6 (2011-10-19)\n++++++++++++++++++\n\n* Session parameter bugfix (params merging).\n\n\n0.6.5 (2011-10-18)\n++++++++++++++++++\n\n* Offline (fast) test suite.\n* Session dictionary argument merging.\n\n\n0.6.4 (2011-10-13)\n++++++++++++++++++\n\n* Automatic decoding of unicode, based on HTTP Headers.\n* New ``decode_unicode`` setting.\n* Removal of ``r.read/close`` methods.\n* New ``r.faw`` interface for advanced response usage.*\n* Automatic expansion of parameterized headers.\n\n\n0.6.3 (2011-10-13)\n++++++++++++++++++\n\n* Beautiful ``requests.async`` module, for making async requests w/ gevent.\n\n\n0.6.2 (2011-10-09)\n++++++++++++++++++\n\n* GET/HEAD obeys allow_redirects=False.\n\n\n0.6.1 (2011-08-20)\n++++++++++++++++++\n\n* Enhanced status codes experience ``\\o/``\n* Set a maximum number of redirects (``settings.max_redirects``)\n* Full Unicode URL support\n* Support for protocol-less redirects.\n* Allow for arbitrary request types.\n* Bugfixes\n\n\n0.6.0 (2011-08-17)\n++++++++++++++++++\n\n* New callback hook system\n* New persistent sessions object and context manager\n* Transparent Dict-cookie handling\n* Status code reference object\n* Removed Response.cached\n* Added Response.request\n* All args are kwargs\n* Relative redirect support\n* HTTPError handling improvements\n* Improved https testing\n* Bugfixes\n\n\n0.5.1 (2011-07-23)\n++++++++++++++++++\n\n* International Domain Name Support!\n* Access headers without fetching entire body (``read()``)\n* Use lists as dicts for parameters\n* Add Forced Basic Authentication\n* Forced Basic is default authentication type\n* ``python-requests.org`` default User-Agent header\n* CaseInsensitiveDict lower-case caching\n* Response.history bugfix\n\n\n0.5.0 (2011-06-21)\n++++++++++++++++++\n\n* PATCH Support\n* Support for Proxies\n* HTTPBin Test Suite\n* Redirect Fixes\n* settings.verbose stream writing\n* Querystrings for all methods\n* URLErrors (Connection Refused, Timeout, Invalid URLs) are treated as explicitly raised\n ``r.requests.get('hwe://blah'); r.raise_for_status()``\n\n\n0.4.1 (2011-05-22)\n++++++++++++++++++\n\n* Improved Redirection Handling\n* New 'allow_redirects' param for following non-GET/HEAD Redirects\n* Settings module refactoring\n\n\n0.4.0 (2011-05-15)\n++++++++++++++++++\n\n* Response.history: list of redirected responses\n* Case-Insensitive Header Dictionaries!\n* Unicode URLs\n\n\n0.3.4 (2011-05-14)\n++++++++++++++++++\n\n* Urllib2 HTTPAuthentication Recursion fix (Basic/Digest)\n* Internal Refactor\n* Bytes data upload Bugfix\n\n\n\n0.3.3 (2011-05-12)\n++++++++++++++++++\n\n* Request timeouts\n* Unicode url-encoded data\n* Settings context manager and module\n\n\n0.3.2 (2011-04-15)\n++++++++++++++++++\n\n* Automatic Decompression of GZip Encoded Content\n* AutoAuth Support for Tupled HTTP Auth\n\n\n0.3.1 (2011-04-01)\n++++++++++++++++++\n\n* Cookie Changes\n* Response.read()\n* Poster fix\n\n\n0.3.0 (2011-02-25)\n++++++++++++++++++\n\n* Automatic Authentication API Change\n* Smarter Query URL Parameterization\n* Allow file uploads and POST data together\n* New Authentication Manager System\n - Simpler Basic HTTP System\n - Supports all build-in urllib2 Auths\n - Allows for custom Auth Handlers\n\n\n0.2.4 (2011-02-19)\n++++++++++++++++++\n\n* Python 2.5 Support\n* PyPy-c v1.4 Support\n* Auto-Authentication tests\n* Improved Request object constructor\n\n0.2.3 (2011-02-15)\n++++++++++++++++++\n\n* New HTTPHandling Methods\n - Response.__nonzero__ (false if bad HTTP Status)\n - Response.ok (True if expected HTTP Status)\n - Response.error (Logged HTTPError if bad HTTP Status)\n - Response.raise_for_status() (Raises stored HTTPError)\n\n\n0.2.2 (2011-02-14)\n++++++++++++++++++\n\n* Still handles request in the event of an HTTPError. (Issue #2)\n* Eventlet and Gevent Monkeypatch support.\n* Cookie Support (Issue #1)\n\n\n0.2.1 (2011-02-14)\n++++++++++++++++++\n\n* Added file attribute to POST and PUT requests for multipart-encode file uploads.\n* Added Request.url attribute for context and redirects\n\n\n0.2.0 (2011-02-14)\n++++++++++++++++++\n\n* Birth!\n\n\n0.0.1 (2011-02-13)\n++++++++++++++++++\n\n* Frustration\n* Conception", "release_url": "http://pypi.python.org/pypi/requests/2.8.1", "downloads": { "last_month": 5397123, "last_week": 1226167, "last_day": 232917 }, "_pypi_ordering": 98, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5" ], "name": "requests", "bugtrack_url": null, "license": "Apache 2.0", "summary": "Python HTTP for Humans.", "home_page": "http://python-requests.org", "cheesecake_installability_id": null }, "releases": { "1.0.4": [ { "has_sig": false, "upload_time": "2012-12-23T07:45:10", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.0.4.tar.gz", "md5_digest": "0b7448f9e1a077a7218720575003a1b6", "downloads": 112618, "filename": "requests-1.0.4.tar.gz", "packagetype": "sdist", "size": 336280 } ], "1.0.0": [ { "has_sig": false, "upload_time": "2012-12-17T15:00:05", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.0.0.tar.gz", "md5_digest": "099c9035c4b30a7ae5484b1beabc7407", "downloads": 16077, "filename": "requests-1.0.0.tar.gz", "packagetype": "sdist", "size": 335548 } ], "1.0.1": [ { "has_sig": false, "upload_time": "2012-12-17T18:53:51", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.0.1.tar.gz", "md5_digest": "2e938f26f2bdf2899862c751bfa7eff5", "downloads": 3679, "filename": "requests-1.0.1.tar.gz", "packagetype": "sdist", "size": 335625 } ], "1.0.2": [ { "has_sig": false, "upload_time": "2012-12-17T19:04:31", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.0.2.tar.gz", "md5_digest": "e5c1a5a5472cd61f144743dd25a2a29f", "downloads": 6921, "filename": "requests-1.0.2.tar.gz", "packagetype": "sdist", "size": 335653 } ], "1.0.3": [ { "has_sig": false, "upload_time": "2012-12-18T09:51:12", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.0.3.tar.gz", "md5_digest": "a3169a33973d4b5b51843ead01c5e999", "downloads": 51762, "filename": "requests-1.0.3.tar.gz", "packagetype": "sdist", "size": 335757 } ], "0.3.2": [ { "has_sig": false, "upload_time": "2011-04-15T23:30:50", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.3.2.tar.gz", "md5_digest": "bde777f4c5b7bbb09033901c443962b3", "downloads": 5216, "filename": "requests-0.3.2.tar.gz", "packagetype": "sdist", "size": 15515 } ], "0.3.3": [ { "has_sig": false, "upload_time": "2011-05-12T10:03:24", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.3.3.tar.gz", "md5_digest": "84c762c116617ba4dd03c19e2b61eb53", "downloads": 4934, "filename": "requests-0.3.3.tar.gz", "packagetype": "sdist", "size": 18995 } ], "0.3.0": [ { "has_sig": false, "upload_time": "2011-02-25T14:58:38", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.3.0.tar.gz", "md5_digest": "aa1306575a78ba8b5e625dd2645d2ef0", "downloads": 5249, "filename": "requests-0.3.0.tar.gz", "packagetype": "sdist", "size": 15021 } ], "0.3.1": [ { "has_sig": false, "upload_time": "2011-04-01T20:55:03", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.3.1.tar.gz", "md5_digest": "3f4701e2ab414cd7018804a70328c527", "downloads": 5143, "filename": "requests-0.3.1.tar.gz", "packagetype": "sdist", "size": 15275 } ], "0.3.4": [ { "has_sig": false, "upload_time": "2011-05-14T20:30:44", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.3.4.tar.gz", "md5_digest": "55152cc2b135bc8989dc4fa279295f8b", "downloads": 4894, "filename": "requests-0.3.4.tar.gz", "packagetype": "sdist", "size": 19773 } ], "0.12.01": [], "2.8.1": [ { "has_sig": false, "upload_time": "2015-10-13T12:56:41", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.8.1-py2.py3-none-any.whl", "md5_digest": "46f1d621daa3ab38958a42f51478b1ee", "downloads": 3302171, "filename": "requests-2.8.1-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 497953 }, { "has_sig": false, "upload_time": "2015-10-13T12:56:34", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.8.1.tar.gz", "md5_digest": "a27ea3d72d7822906ddce5e252d6add9", "downloads": 2462803, "filename": "requests-2.8.1.tar.gz", "packagetype": "sdist", "size": 480803 } ], "2.8.0": [ { "has_sig": false, "upload_time": "2015-10-06T14:47:57", "comment_text": "", "python_version": "py2.py3", "url": "https://pypi.python.org/packages/py2.py3/r/requests/requests-2.8.0-py2.py3-none-any.whl", "md5_digest": "52236eb6f886db4d2afba43775c97050", "downloads": 416112, "filename": "requests-2.8.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 476582 }, { "has_sig": false, "upload_time": "2015-10-06T14:48:08", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.8.0.tar.gz", "md5_digest": "3ec7198fc935d83c3eacff1ed4095ce4", "downloads": 388624, "filename": "requests-2.8.0.tar.gz", "packagetype": "sdist", "size": 457879 } ], "0.6.5": [ { "has_sig": false, "upload_time": "2011-10-19T07:30:59", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.6.5.tar.gz", "md5_digest": "52f8bc956e027c8a0eb2684f6928169d", "downloads": 4778, "filename": "requests-0.6.5.tar.gz", "packagetype": "sdist", "size": 30647 } ], "2.0.1": [ { "has_sig": false, "upload_time": "2013-11-15T19:12:20", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.0.1-py2.py3-none-any.whl", "md5_digest": "d524f9a38a29efe1732fd130e5ebe433", "downloads": 316311, "filename": "requests-2.0.1-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 439330 }, { "has_sig": false, "upload_time": "2013-10-24T14:33:21", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.0.1.tar.gz", "md5_digest": "38e61c2856d2ba2782286730241975e6", "downloads": 1450321, "filename": "requests-2.0.1.tar.gz", "packagetype": "sdist", "size": 412648 } ], "2.0.0": [ { "has_sig": false, "upload_time": "2013-11-15T19:09:51", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.0.0-py2.py3-none-any.whl", "md5_digest": "6af9c16dbddd2fc751ae4f1606d041e8", "downloads": 299730, "filename": "requests-2.0.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 391141 }, { "has_sig": false, "upload_time": "2013-09-24T18:39:33", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.0.0.tar.gz", "md5_digest": "856fc825c17483e25fd55db115028e3f", "downloads": 1063100, "filename": "requests-2.0.0.tar.gz", "packagetype": "sdist", "size": 362994 } ], "0.6.6": [ { "has_sig": false, "upload_time": "2011-10-19T09:39:56", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.6.6.tar.gz", "md5_digest": "2180dacebc0e30ba730d083739907af6", "downloads": 6671, "filename": "requests-0.6.6.tar.gz", "packagetype": "sdist", "size": 30809 } ], "2.2.1": [ { "has_sig": false, "upload_time": "2014-01-23T18:26:15", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.2.1-py2.py3-none-any.whl", "md5_digest": "1e38addb978e50bd86f62bda53956b03", "downloads": 2658147, "filename": "requests-2.2.1-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 625382 }, { "has_sig": false, "upload_time": "2014-01-23T18:26:12", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.2.1.tar.gz", "md5_digest": "ac27081135f58d1a43e4fb38258d6f4e", "downloads": 3407542, "filename": "requests-2.2.1.tar.gz", "packagetype": "sdist", "size": 421978 } ], "2.2.0": [ { "has_sig": false, "upload_time": "2014-01-09T19:33:37", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.2.0-py2.py3-none-any.whl", "md5_digest": "8f989615bb0d276d5f3158e7efab494c", "downloads": 144823, "filename": "requests-2.2.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 623932 }, { "has_sig": false, "upload_time": "2014-01-09T19:33:32", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.2.0.tar.gz", "md5_digest": "4d2e17221d478ece045e2e81cdb177f5", "downloads": 392673, "filename": "requests-2.2.0.tar.gz", "packagetype": "sdist", "size": 421997 } ], "0.6.3": [ { "has_sig": false, "upload_time": "2011-10-14T03:35:13", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.6.3.tar.gz", "md5_digest": "35a954ae85b358e498fb0e602f1dce9d", "downloads": 4769, "filename": "requests-0.6.3.tar.gz", "packagetype": "sdist", "size": 26606 } ], "0.6.2": [ { "has_sig": false, "upload_time": "2011-10-09T13:12:45", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.6.2.tar.gz", "md5_digest": "0583bb5393b9cfcb022dc2aef7d6ffc8", "downloads": 7850, "filename": "requests-0.6.2.tar.gz", "packagetype": "sdist", "size": 26524 } ], "0.6.1": [ { "has_sig": false, "upload_time": "2011-08-21T00:25:37", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.6.1.tar.gz", "md5_digest": "07770334d48bd69ede1cc28cd0dd7680", "downloads": 21399, "filename": "requests-0.6.1.tar.gz", "packagetype": "sdist", "size": 26107 } ], "0.6.0": [ { "has_sig": false, "upload_time": "2011-08-17T10:33:05", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.6.0.tar.gz", "md5_digest": "235e9fb6bfd71a48c0f00c0d5aef8896", "downloads": 6029, "filename": "requests-0.6.0.tar.gz", "packagetype": "sdist", "size": 25692 } ], "0.11.1": [ { "has_sig": false, "upload_time": "2012-03-31T05:47:56", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.11.1.tar.gz", "md5_digest": "c903c32a0e1f04889e693da8e9c71872", "downloads": 38150, "filename": "requests-0.11.1.tar.gz", "packagetype": "sdist", "size": 63100 } ], "0.11.2": [ { "has_sig": false, "upload_time": "2012-04-23T04:29:36", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.11.2.tar.gz", "md5_digest": "5acd23600c897bf1560dca18005b428c", "downloads": 36476, "filename": "requests-0.11.2.tar.gz", "packagetype": "sdist", "size": 71080 } ], "0.6.4": [ { "has_sig": false, "upload_time": "2011-10-14T04:23:31", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.6.4.tar.gz", "md5_digest": "e0eec314178ad9a7bb14f2ec32f35ba3", "downloads": 9277, "filename": "requests-0.6.4.tar.gz", "packagetype": "sdist", "size": 30212 } ], "0.13.8": [ { "has_sig": false, "upload_time": "2012-08-20T15:24:42", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.8.tar.gz", "md5_digest": "d01596bd344db94763b2e4dfaa7bc7b9", "downloads": 26725, "filename": "requests-0.13.8.tar.gz", "packagetype": "sdist", "size": 522140 } ], "0.13.9": [ { "has_sig": false, "upload_time": "2012-08-25T15:26:50", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.9.tar.gz", "md5_digest": "66d52b8f47be517fc91a6e18d6b9ce82", "downloads": 436286, "filename": "requests-0.13.9.tar.gz", "packagetype": "sdist", "size": 522477 } ], "0.13.6": [ { "has_sig": false, "upload_time": "2012-08-06T08:46:22", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.6.tar.gz", "md5_digest": "9ea0f38cc4bf444be5a4c90f127211f2", "downloads": 83586, "filename": "requests-0.13.6.tar.gz", "packagetype": "sdist", "size": 520031 } ], "0.13.7": [ { "has_sig": false, "upload_time": "2012-08-19T00:47:48", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.7.tar.gz", "md5_digest": "9212044f915d44fe3010cb923c0e08e5", "downloads": 20167, "filename": "requests-0.13.7.tar.gz", "packagetype": "sdist", "size": 521660 } ], "0.13.4": [ { "has_sig": false, "upload_time": "2012-07-27T08:22:09", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.4.tar.gz", "md5_digest": "286cd3352509691e81c520accc5b9e48", "downloads": 4033, "filename": "requests-0.13.4.tar.gz", "packagetype": "sdist", "size": 519515 } ], "0.13.5": [ { "has_sig": false, "upload_time": "2012-07-27T09:23:41", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.5.tar.gz", "md5_digest": "805fd122b4cfd224e15ff2f5288c5ba0", "downloads": 89067, "filename": "requests-0.13.5.tar.gz", "packagetype": "sdist", "size": 519553 } ], "0.13.2": [ { "has_sig": false, "upload_time": "2012-06-29T02:37:41", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.2.tar.gz", "md5_digest": "fac5635391778e2394a411d37e69ae5e", "downloads": 117591, "filename": "requests-0.13.2.tar.gz", "packagetype": "sdist", "size": 514484 } ], "0.13.3": [ { "has_sig": false, "upload_time": "2012-07-12T23:20:43", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.3.tar.gz", "md5_digest": "54387d7df6c69580b906dcb5a2bd0724", "downloads": 144912, "filename": "requests-0.13.3.tar.gz", "packagetype": "sdist", "size": 515192 } ], "0.13.0": [ { "has_sig": false, "upload_time": "2012-05-30T02:54:18", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.0.tar.gz", "md5_digest": "7d41e51c273806456faab61370d5147e", "downloads": 40543, "filename": "requests-0.13.0.tar.gz", "packagetype": "sdist", "size": 68172 } ], "0.13.1": [ { "has_sig": false, "upload_time": "2012-06-08T04:22:28", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.13.1.tar.gz", "md5_digest": "31a08091feeefe60817e45122d933219", "downloads": 121874, "filename": "requests-0.13.1.tar.gz", "packagetype": "sdist", "size": 68474 } ], "0.8.9": [ { "has_sig": false, "upload_time": "2011-12-28T10:34:17", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.9.tar.gz", "md5_digest": "ff5b3bf5bc3ad19930d3f3afe51f182b", "downloads": 10963, "filename": "requests-0.8.9.tar.gz", "packagetype": "sdist", "size": 55153 } ], "0.8.8": [ { "has_sig": false, "upload_time": "2011-12-28T09:55:45", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.8.tar.gz", "md5_digest": "bfb182cfd3ed839b97744c553b87f502", "downloads": 5368, "filename": "requests-0.8.8.tar.gz", "packagetype": "sdist", "size": 54212 } ], "0.14.2": [ { "has_sig": false, "upload_time": "2012-10-27T15:08:51", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.14.2.tar.gz", "md5_digest": "488508ba3e8270992ad5b3fb54d364ca", "downloads": 918155, "filename": "requests-0.14.2.tar.gz", "packagetype": "sdist", "size": 361488 } ], "0.8.5": [ { "has_sig": false, "upload_time": "2011-12-14T16:43:21", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.5.tar.gz", "md5_digest": "5f2975ee9e57f4ea000e5a3f50fc85d1", "downloads": 6281, "filename": "requests-0.8.5.tar.gz", "packagetype": "sdist", "size": 52351 } ], "0.8.4": [ { "has_sig": false, "upload_time": "2011-12-11T17:40:28", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.4.tar.gz", "md5_digest": "642e5c70250989e4feda9c50be57b100", "downloads": 13119, "filename": "requests-0.8.4.tar.gz", "packagetype": "sdist", "size": 52100 } ], "0.8.7": [ { "has_sig": false, "upload_time": "2011-12-24T09:18:54", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.7.tar.gz", "md5_digest": "e4d4ee3a90396908bd04b50bf2136617", "downloads": 5369, "filename": "requests-0.8.7.tar.gz", "packagetype": "sdist", "size": 53578 } ], "0.8.6": [ { "has_sig": false, "upload_time": "2011-12-19T01:18:29", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.6.tar.gz", "md5_digest": "21b03926ab38417a704ebce57972571a", "downloads": 7981, "filename": "requests-0.8.6.tar.gz", "packagetype": "sdist", "size": 52670 } ], "0.8.1": [ { "has_sig": false, "upload_time": "2011-11-15T16:01:47", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.1.tar.gz", "md5_digest": "6135f837fbd113fc62904c60dcc5c70d", "downloads": 8542, "filename": "requests-0.8.1.tar.gz", "packagetype": "sdist", "size": 39046 } ], "0.8.0": [ { "has_sig": false, "upload_time": "2011-11-13T06:52:10", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.0.tar.gz", "md5_digest": "64dc0095cb645aa7f0083957950d524d", "downloads": 5776, "filename": "requests-0.8.0.tar.gz", "packagetype": "sdist", "size": 38785 } ], "0.8.3": [ { "has_sig": false, "upload_time": "2011-11-27T16:44:51", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.3.tar.gz", "md5_digest": "93e4cd27ab646fb613a926fede1cc4f5", "downloads": 12461, "filename": "requests-0.8.3.tar.gz", "packagetype": "sdist", "size": 51252 } ], "0.8.2": [ { "has_sig": false, "upload_time": "2011-11-19T22:28:31", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.8.2.tar.gz", "md5_digest": "bdbbd7f45688e23e87eec52835959943", "downloads": 30687, "filename": "requests-0.8.2.tar.gz", "packagetype": "sdist", "size": 51162 } ], "0.4.1": [ { "has_sig": false, "upload_time": "2011-05-25T18:54:05", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.4.1.tar.gz", "md5_digest": "812ff0ce63d14f7b940bacd880d54ee0", "downloads": 9781, "filename": "requests-0.4.1.tar.gz", "packagetype": "sdist", "size": 18443 } ], "0.4.0": [ { "has_sig": false, "upload_time": "2011-05-15T05:58:43", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.4.0.tar.gz", "md5_digest": "77a7a7edd54169c6fa7ace49dcb0b20c", "downloads": 5713, "filename": "requests-0.4.0.tar.gz", "packagetype": "sdist", "size": 17194 } ], "2.4.3": [ { "has_sig": false, "upload_time": "2014-10-06T09:44:49", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.4.3-py2.py3-none-any.whl", "md5_digest": "0a66a9c4c22272680430fbb9fb4ca34f", "downloads": 2501282, "filename": "requests-2.4.3-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 459464 }, { "has_sig": false, "upload_time": "2014-10-06T09:44:44", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.4.3.tar.gz", "md5_digest": "02214b3a179e445545de4b7a98d3dd17", "downloads": 2827852, "filename": "requests-2.4.3.tar.gz", "packagetype": "sdist", "size": 438132 } ], "2.4.2": [ { "has_sig": false, "upload_time": "2014-10-05T17:15:53", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.4.2-py2.py3-none-any.whl", "md5_digest": "f49f34b1fcdef6b557964deea1a80cf3", "downloads": 26784, "filename": "requests-2.4.2-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 459326 }, { "has_sig": false, "upload_time": "2014-10-05T17:15:45", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.4.2.tar.gz", "md5_digest": "a2476d2dd83a0520847f216ce0b5f9d1", "downloads": 46933, "filename": "requests-2.4.2.tar.gz", "packagetype": "sdist", "size": 437898 } ], "2.4.1": [ { "has_sig": false, "upload_time": "2014-09-09T16:35:12", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.4.1-py2.py3-none-any.whl", "md5_digest": "19d5413dc71309e4fb1f8103b8eb99ce", "downloads": 921215, "filename": "requests-2.4.1-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 458354 }, { "has_sig": false, "upload_time": "2014-09-09T16:35:08", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.4.1.tar.gz", "md5_digest": "931461f761c70708c46ea65b7889da58", "downloads": 1352590, "filename": "requests-2.4.1.tar.gz", "packagetype": "sdist", "size": 436872 } ], "2.4.0": [ { "has_sig": false, "upload_time": "2014-08-29T14:32:48", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.4.0-py2.py3-none-any.whl", "md5_digest": "47948d2fb3f2aa04235e6f637814b226", "downloads": 308274, "filename": "requests-2.4.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 457810 }, { "has_sig": false, "upload_time": "2014-08-29T14:32:45", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.4.0.tar.gz", "md5_digest": "99b830d1afe2e5920adbea0fe3120948", "downloads": 597502, "filename": "requests-2.4.0.tar.gz", "packagetype": "sdist", "size": 436334 } ], "2.6.1": [ { "has_sig": false, "upload_time": "2015-04-23T02:27:04", "comment_text": "", "python_version": "py2.py3", "url": "https://pypi.python.org/packages/py2.py3/r/requests/requests-2.6.1-py2.py3-none-any.whl", "md5_digest": "adb8e91b3367bc0417ef1e4a6dced9b1", "downloads": 47681, "filename": "requests-2.6.1-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 469962 }, { "has_sig": false, "upload_time": "2015-04-23T02:27:12", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.6.1.tar.gz", "md5_digest": "da6e487f89e6a531699b7fd97ff182af", "downloads": 43134, "filename": "requests-2.6.1.tar.gz", "packagetype": "sdist", "size": 450975 } ], "2.6.0": [ { "has_sig": false, "upload_time": "2015-03-14T16:44:37", "comment_text": "", "python_version": "py2.py3", "url": "https://pypi.python.org/packages/py2.py3/r/requests/requests-2.6.0-py2.py3-none-any.whl", "md5_digest": "3ab1972bbaf2802d94516fb86b9b0d0b", "downloads": 2003507, "filename": "requests-2.6.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 469802 }, { "has_sig": false, "upload_time": "2015-03-14T16:44:48", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.6.0.tar.gz", "md5_digest": "25287278fa3ea106207461112bb37050", "downloads": 2402265, "filename": "requests-2.6.0.tar.gz", "packagetype": "sdist", "size": 450389 } ], "2.6.2": [ { "has_sig": false, "upload_time": "2015-04-23T16:30:52", "comment_text": "", "python_version": "py2.py3", "url": "https://pypi.python.org/packages/py2.py3/r/requests/requests-2.6.2-py2.py3-none-any.whl", "md5_digest": "36746c275589b2154307bbcc6d28320a", "downloads": 839011, "filename": "requests-2.6.2-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 470140 }, { "has_sig": false, "upload_time": "2015-04-23T16:31:01", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.6.2.tar.gz", "md5_digest": "0d703e5be558566e0f8c37f960d95372", "downloads": 598600, "filename": "requests-2.6.2.tar.gz", "packagetype": "sdist", "size": 451109 } ], "1.1.0": [ { "has_sig": false, "upload_time": "2013-01-10T07:13:41", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.1.0.tar.gz", "md5_digest": "a0158815af244c32041a3147ee09abf3", "downloads": 1423824, "filename": "requests-1.1.0.tar.gz", "packagetype": "sdist", "size": 337229 } ], "0.2.4": [ { "has_sig": false, "upload_time": "2011-02-19T07:03:14", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.2.4.tar.gz", "md5_digest": "62dbe8cf12bc1ccd03776e74f59e9ef6", "downloads": 5627, "filename": "requests-0.2.4.tar.gz", "packagetype": "sdist", "size": 13653 } ], "0.2.3": [ { "has_sig": false, "upload_time": "2011-02-15T15:47:29", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.2.3.tar.gz", "md5_digest": "102243646fc0cffdc82269f4bb5c6d5d", "downloads": 5009, "filename": "requests-0.2.3.tar.gz", "packagetype": "sdist", "size": 13255 } ], "0.2.2": [ { "has_sig": false, "upload_time": "2011-02-14T18:58:40", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.2.2.tar.gz", "md5_digest": "a703489b1a4a650698ddcf84857360c6", "downloads": 4960, "filename": "requests-0.2.2.tar.gz", "packagetype": "sdist", "size": 13049 } ], "0.2.1": [ { "has_sig": false, "upload_time": "2011-02-14T16:38:12", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.2.1.tar.gz", "md5_digest": "7e9590f3985ece46fc8306e906b458c7", "downloads": 4947, "filename": "requests-0.2.1.tar.gz", "packagetype": "sdist", "size": 12715 } ], "0.2.0": [ { "has_sig": false, "upload_time": "2011-02-14T08:49:42", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.2.0.tar.gz", "md5_digest": "637ae94cb6f2f1d9ea9020293055964a", "downloads": 5066, "filename": "requests-0.2.0.tar.gz", "packagetype": "sdist", "size": 5533 } ], "0.14.1": [ { "has_sig": false, "upload_time": "2012-10-01T17:30:05", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.14.1.tar.gz", "md5_digest": "3de30600072cbc7214ae342d1d08aa46", "downloads": 299590, "filename": "requests-0.14.1.tar.gz", "packagetype": "sdist", "size": 523254 } ], "2.1.0": [ { "has_sig": false, "upload_time": "2013-12-05T22:51:41", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.1.0-py2.py3-none-any.whl", "md5_digest": "0848cbc0cc7edd150cb8d6ddc25ca906", "downloads": 802779, "filename": "requests-2.1.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 445280 }, { "has_sig": false, "upload_time": "2013-12-05T22:51:38", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.1.0.tar.gz", "md5_digest": "28543001831f46b1ff40686ebc027deb", "downloads": 980428, "filename": "requests-2.1.0.tar.gz", "packagetype": "sdist", "size": 420289 } ], "0.14.0": [ { "has_sig": false, "upload_time": "2012-09-02T08:50:39", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.14.0.tar.gz", "md5_digest": "a809c747e4f09b92147721ebc3e23dd6", "downloads": 502482, "filename": "requests-0.14.0.tar.gz", "packagetype": "sdist", "size": 523133 } ], "0.10.1": [ { "has_sig": false, "upload_time": "2012-01-23T08:22:52", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.10.1.tar.gz", "md5_digest": "699147d2143bff95238befa58980b912", "downloads": 26671, "filename": "requests-0.10.1.tar.gz", "packagetype": "sdist", "size": 63234 } ], "0.10.0": [ { "has_sig": false, "upload_time": "2012-01-22T05:08:17", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.10.0.tar.gz", "md5_digest": "c90a48af18eb4170dbe4832c1104440c", "downloads": 10399, "filename": "requests-0.10.0.tar.gz", "packagetype": "sdist", "size": 62046 } ], "0.10.3": [ { "has_sig": false, "upload_time": "2012-02-20T20:10:57", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.10.3.tar.gz", "md5_digest": "a055af00593f4828c3becd0ccfab503f", "downloads": 4709, "filename": "requests-0.10.3.tar.gz", "packagetype": "sdist", "size": 60493 } ], "0.10.2": [ { "has_sig": false, "upload_time": "2012-02-15T09:48:52", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.10.2.tar.gz", "md5_digest": "47c3cf85a0112d423137b43989663bef", "downloads": 17350, "filename": "requests-0.10.2.tar.gz", "packagetype": "sdist", "size": 60158 } ], "0.0.1": [], "0.10.4": [ { "has_sig": false, "upload_time": "2012-02-20T22:21:31", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.10.4.tar.gz", "md5_digest": "5e465e9e739bcc9f71935ca4e9706168", "downloads": 13489, "filename": "requests-0.10.4.tar.gz", "packagetype": "sdist", "size": 60889 } ], "0.10.7": [ { "has_sig": false, "upload_time": "2012-03-08T01:50:58", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.10.7.tar.gz", "md5_digest": "a3ac9d431981dcfd592fd0f35c499e4a", "downloads": 6757, "filename": "requests-0.10.7.tar.gz", "packagetype": "sdist", "size": 61826 } ], "0.10.6": [ { "has_sig": false, "upload_time": "2012-02-26T05:17:54", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.10.6.tar.gz", "md5_digest": "c889401445de3cbbac98509208a73b83", "downloads": 28073, "filename": "requests-0.10.6.tar.gz", "packagetype": "sdist", "size": 61673 } ], "0.7.6": [ { "has_sig": false, "upload_time": "2011-11-07T20:19:31", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.7.6.tar.gz", "md5_digest": "728b21bf3914d69a4ff1012c66d9b6ba", "downloads": 8941, "filename": "requests-0.7.6.tar.gz", "packagetype": "sdist", "size": 32748 } ], "0.10.8": [ { "has_sig": false, "upload_time": "2012-03-09T17:59:54", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.10.8.tar.gz", "md5_digest": "0fc89a30eef76b2393cbc7ebace91750", "downloads": 97221, "filename": "requests-0.10.8.tar.gz", "packagetype": "sdist", "size": 62201 } ], "0.7.4": [ { "has_sig": false, "upload_time": "2011-10-27T00:36:25", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.7.4.tar.gz", "md5_digest": "c015765399b8c1e309c84ade0d38f07b", "downloads": 10308, "filename": "requests-0.7.4.tar.gz", "packagetype": "sdist", "size": 31873 } ], "0.7.5": [ { "has_sig": false, "upload_time": "2011-11-05T04:32:37", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.7.5.tar.gz", "md5_digest": "9a12281a811ca25d347d806c456d96f1", "downloads": 5899, "filename": "requests-0.7.5.tar.gz", "packagetype": "sdist", "size": 32298 } ], "0.7.2": [ { "has_sig": false, "upload_time": "2011-10-23T21:40:37", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.7.2.tar.gz", "md5_digest": "03eb97ed6aacb4102fd434bbfc13ce17", "downloads": 5630, "filename": "requests-0.7.2.tar.gz", "packagetype": "sdist", "size": 31837 } ], "0.7.3": [ { "has_sig": false, "upload_time": "2011-10-23T23:04:13", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.7.3.tar.gz", "md5_digest": "267f6f7d1109775d24a288f798e3ab4a", "downloads": 9194, "filename": "requests-0.7.3.tar.gz", "packagetype": "sdist", "size": 31805 } ], "0.7.0": [ { "has_sig": false, "upload_time": "2011-10-23T03:33:24", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.7.0.tar.gz", "md5_digest": "83a1a7d79218756efd19b254eeb6b1f0", "downloads": 4993, "filename": "requests-0.7.0.tar.gz", "packagetype": "sdist", "size": 31260 } ], "0.7.1": [ { "has_sig": false, "upload_time": "2011-10-23T21:19:22", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.7.1.tar.gz", "md5_digest": "4821c6902d8e83c910c69c6492388e5f", "downloads": 4675, "filename": "requests-0.7.1.tar.gz", "packagetype": "sdist", "size": 31804 } ], "0.12.1": [ { "has_sig": false, "upload_time": "2012-05-08T07:21:59", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.12.1.tar.gz", "md5_digest": "fe9e0515d09733d0eb9e2031c03401b2", "downloads": 131145, "filename": "requests-0.12.1.tar.gz", "packagetype": "sdist", "size": 78245 } ], "0.12.0": [ { "has_sig": false, "upload_time": "2012-05-03T01:18:47", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.12.0.tar.gz", "md5_digest": "c38bacf4d6a065f3c47463e63efdfb5a", "downloads": 12488, "filename": "requests-0.12.0.tar.gz", "packagetype": "sdist", "size": 76859 } ], "0.5.0": [ { "has_sig": false, "upload_time": "2011-06-22T04:44:39", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.5.0.tar.gz", "md5_digest": "6dfdc1688217d774d524e056ec6605a6", "downloads": 8450, "filename": "requests-0.5.0.tar.gz", "packagetype": "sdist", "size": 21945 } ], "0.5.1": [ { "has_sig": false, "upload_time": "2011-07-24T05:01:45", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.5.1.tar.gz", "md5_digest": "33a6e65d6a4e5b2d91df76256f607b81", "downloads": 7189, "filename": "requests-0.5.1.tar.gz", "packagetype": "sdist", "size": 23080 } ], "0.9.0": [ { "has_sig": false, "upload_time": "2011-12-28T10:51:35", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.9.0.tar.gz", "md5_digest": "5f6f03ec76f68a7a3f35120ab5a6c589", "downloads": 16697, "filename": "requests-0.9.0.tar.gz", "packagetype": "sdist", "size": 55217 } ], "0.9.1": [ { "has_sig": false, "upload_time": "2012-01-06T07:11:02", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.9.1.tar.gz", "md5_digest": "8ed4667edb5d57945b74a9137adbb8bd", "downloads": 14372, "filename": "requests-0.9.1.tar.gz", "packagetype": "sdist", "size": 55547 } ], "0.9.2": [ { "has_sig": false, "upload_time": "2012-01-19T03:39:58", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.9.2.tar.gz", "md5_digest": "65b36d99a4d2f78a22f08c95d2475e33", "downloads": 4700, "filename": "requests-0.9.2.tar.gz", "packagetype": "sdist", "size": 60967 } ], "0.9.3": [ { "has_sig": false, "upload_time": "2012-01-19T16:51:33", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-0.9.3.tar.gz", "md5_digest": "b13b6fbfa8fc3fc3c25bae300748053f", "downloads": 11556, "filename": "requests-0.9.3.tar.gz", "packagetype": "sdist", "size": 61006 } ], "2.3.0": [ { "has_sig": false, "upload_time": "2014-05-16T17:57:05", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.3.0-py2.py3-none-any.whl", "md5_digest": "f2d850fd48fc10a93aa03d69b87b96b4", "downloads": 4032003, "filename": "requests-2.3.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 452902 }, { "has_sig": false, "upload_time": "2014-05-16T17:57:02", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.3.0.tar.gz", "md5_digest": "7449ffdc8ec9ac37bbcd286003c80f00", "downloads": 4723580, "filename": "requests-2.3.0.tar.gz", "packagetype": "sdist", "size": 429521 } ], "1.2.2": [ { "has_sig": false, "upload_time": "2013-05-21T21:44:44", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.2.2.tar.gz", "md5_digest": "1f655ab7f2aa7447a1657ed69786f436", "downloads": 219258, "filename": "requests-1.2.2.tar.gz", "packagetype": "sdist", "size": 348851 } ], "1.2.3": [ { "has_sig": false, "upload_time": "2013-05-25T16:48:36", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.2.3.tar.gz", "md5_digest": "adbd3f18445f7fe5e77f65c502e264fb", "downloads": 3278337, "filename": "requests-1.2.3.tar.gz", "packagetype": "sdist", "size": 348854 } ], "1.2.0": [ { "has_sig": false, "upload_time": "2013-03-31T05:28:47", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.2.0.tar.gz", "md5_digest": "22af2682233770e5468a986f451c51c0", "downloads": 896245, "filename": "requests-1.2.0.tar.gz", "packagetype": "sdist", "size": 341511 } ], "1.2.1": [ { "has_sig": false, "upload_time": "2013-05-20T20:11:09", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-1.2.1.tar.gz", "md5_digest": "4d019670b94b17e329007d64e67e045e", "downloads": 8076, "filename": "requests-1.2.1.tar.gz", "packagetype": "sdist", "size": 348710 } ], "2.5.2": [ { "has_sig": false, "upload_time": "2015-02-23T22:37:39", "comment_text": "", "python_version": "py2.py3", "url": "https://pypi.python.org/packages/py2.py3/r/requests/requests-2.5.2-py2.py3-none-any.whl", "md5_digest": "7e72dfe8ed9d4ce5fd9dd9d799b3add1", "downloads": 162592, "filename": "requests-2.5.2-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 474275 }, { "has_sig": false, "upload_time": "2015-02-23T22:37:46", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.5.2.tar.gz", "md5_digest": "424e2469202c9bace4e8bf4642d4217a", "downloads": 53980, "filename": "requests-2.5.2.tar.gz", "packagetype": "sdist", "size": 455688 } ], "2.5.3": [ { "has_sig": false, "upload_time": "2015-02-24T16:33:49", "comment_text": "", "python_version": "py2.py3", "url": "https://pypi.python.org/packages/py2.py3/r/requests/requests-2.5.3-py2.py3-none-any.whl", "md5_digest": "233249f4627ac5481c948e494d2a090e", "downloads": 873743, "filename": "requests-2.5.3-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 468593 }, { "has_sig": false, "upload_time": "2015-02-24T16:33:58", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.5.3.tar.gz", "md5_digest": "23bf4fcc89ea8d353eb5353bb4a475b1", "downloads": 874483, "filename": "requests-2.5.3.tar.gz", "packagetype": "sdist", "size": 448318 } ], "2.5.0": [ { "has_sig": false, "upload_time": "2014-12-01T23:27:51", "comment_text": "", "python_version": "py2.py3", "url": "https://pypi.python.org/packages/py2.py3/r/requests/requests-2.5.0-py2.py3-none-any.whl", "md5_digest": "9d29a8a0210c236d9329bed49277b3fa", "downloads": 877099, "filename": "requests-2.5.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 464196 }, { "has_sig": false, "upload_time": "2014-12-01T23:27:58", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.5.0.tar.gz", "md5_digest": "b8bf3ddca75e7ecf1b6776da1e6e3385", "downloads": 1056687, "filename": "requests-2.5.0.tar.gz", "packagetype": "sdist", "size": 443222 } ], "2.5.1": [ { "has_sig": false, "upload_time": "2014-12-23T17:55:59", "comment_text": "", "python_version": "py2.py3", "url": "https://pypi.python.org/packages/py2.py3/r/requests/requests-2.5.1-py2.py3-none-any.whl", "md5_digest": "11dc91bc96c5c5e0b566ce8f9c9644ab", "downloads": 1893375, "filename": "requests-2.5.1-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 464421 }, { "has_sig": false, "upload_time": "2014-12-23T17:56:08", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.5.1.tar.gz", "md5_digest": "c270eb5551a02e8ab7a4cbb83e22af2e", "downloads": 2583555, "filename": "requests-2.5.1.tar.gz", "packagetype": "sdist", "size": 443633 } ], "2.7.0": [ { "has_sig": false, "upload_time": "2015-05-03T15:01:28", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.7.0-py2.py3-none-any.whl", "md5_digest": "564fb256f865a79f977e57b79d31659a", "downloads": 8550789, "filename": "requests-2.7.0-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 470641 }, { "has_sig": false, "upload_time": "2015-05-03T15:01:21", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.7.0.tar.gz", "md5_digest": "29b173fd5fa572ec0764d1fd7b527260", "downloads": 7696812, "filename": "requests-2.7.0.tar.gz", "packagetype": "sdist", "size": 451723 } ] }, "urls": [ { "has_sig": false, "upload_time": "2015-10-13T12:56:41", "comment_text": "", "python_version": "2.7", "url": "https://pypi.python.org/packages/2.7/r/requests/requests-2.8.1-py2.py3-none-any.whl", "md5_digest": "46f1d621daa3ab38958a42f51478b1ee", "downloads": 3302171, "filename": "requests-2.8.1-py2.py3-none-any.whl", "packagetype": "bdist_wheel", "size": 497953 }, { "has_sig": false, "upload_time": "2015-10-13T12:56:34", "comment_text": "", "python_version": "source", "url": "https://pypi.python.org/packages/source/r/requests/requests-2.8.1.tar.gz", "md5_digest": "a27ea3d72d7822906ddce5e252d6add9", "downloads": 2462803, "filename": "requests-2.8.1.tar.gz", "packagetype": "sdist", "size": 480803 } ] } ================================================ FILE: tests/integtest.py ================================================ # Copyright 2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Helper to run integration tests. This is not part of the regular test suite, but a test that is used in a very specific way from the integration tests defined in the Github CI infrastructure. """ import sys def test_assert_python_version(pytestconfig): expected = pytestconfig.getoption("integtest_pyversion") vi = sys.version_info current = f"{vi.major}.{vi.minor}" assert current == expected ================================================ FILE: tests/test_cache/__init__.py ================================================ # Copyright 2015-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Helpers for the Cache tests collection.""" from fades.parsing import NameVerDependency def get_distrib(*dep_ver_pairs): """Build some Distributions with indicated info.""" return [NameVerDependency(dep, ver) for dep, ver in dep_ver_pairs] ================================================ FILE: tests/test_cache/conftest.py ================================================ # Copyright 2015-2019 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades import shutil from pytest import fixture from fades import cache @fixture(scope="function") def venvscache(tmpdir_factory): """Fixture for a cache file for virtualenvs.""" dir_path = tmpdir_factory.mktemp("test") venvs_cache = cache.VEnvsCache(dir_path.join("test_venv_cache")) yield venvs_cache shutil.rmtree(str(dir_path)) ================================================ FILE: tests/test_cache/test_caches.py ================================================ # Copyright 2015-2022 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades from unittest.mock import patch from fades import cache def test_missing_file_pytest(tmp_file): venvscache = cache.VEnvsCache(str(tmp_file)) with patch.object(venvscache, '_select', return_value=None) as mock: resp = venvscache.get_venv('requirements', 'interpreter', uuid='', options='options') mock.assert_called_with([], 'requirements', 'interpreter', uuid='', options='options') assert not resp def test_empty_file_pytest(tmp_file): open(tmp_file, 'wt', encoding='utf8').close() venvscache = cache.VEnvsCache(tmp_file) with patch.object(venvscache, '_select', return_value=None) as mock: resp = venvscache.get_venv('requirements', 'interpreter') mock.assert_called_with([], 'requirements', 'interpreter', uuid='', options=None) assert not resp def test_some_file_content_pytest(tmp_file): with open(tmp_file, 'wt', encoding='utf8') as fh: fh.write('foo\nbar\n') venvscache = cache.VEnvsCache(tmp_file) with patch.object(venvscache, '_select', return_value="resp") as mock: resp = venvscache.get_venv('requirements', 'interpreter', uuid='', options='options') mock.assert_called_with(['foo', 'bar'], 'requirements', 'interpreter', uuid='', options='options') assert resp == 'resp' def test_get_by_uuid_pytest(tmp_file): with open(tmp_file, 'wt', encoding='utf8') as fh: fh.write('foo\nbar\n') venvscache = cache.VEnvsCache(tmp_file) with patch.object(venvscache, '_select', return_value='resp') as mock: resp = venvscache.get_venv(uuid='uuid') mock.assert_called_with(['foo', 'bar'], None, '', uuid='uuid', options=None) assert resp == 'resp' ================================================ FILE: tests/test_cache/test_comparisons.py ================================================ # Copyright 2015-2019 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades import json import pytest from fades import parsing from tests import get_reqs from tests.test_cache import get_distrib @pytest.mark.parametrize("req,installed,expected", [ # Equal ("==5", "5", "ok"), ("==5", "2", None), # Greater than (">5", "4", None), (">5", "5", None), (">5", "6", "ok"), # Greater than or equal (">=5", "4", None), (">=5", "5", "ok"), (">=5", "6", "ok"), # Less than ("<5", "4", "ok"), ("<5", "5", None), ("<5", "6", None), # Less than or equal ("<=5", "4", "ok"), ("<=5", "5", "ok"), ("<=5", "6", None), # Complex cases ("== 2.5", "2.5.0", "ok"), ("> 2.7", "2.12", "ok"), ("> 2.7a0", "2.7", "ok"), ("> 2.7", "2.7a0", None), # Crazy picky (">1.6,<1.9,!=1.9.6", "1.5.0", None), (">1.6,<1.9,!=1.9.6", "1.6.7", "ok"), (">1.6,<1.9,!=1.8.6", "1.8.7", "ok"), (">1.6,<1.9,!=1.9.6", "1.9.6", None), ]) def test_check_versions(venvscache, req, installed, expected): """The comparison in the selection.""" reqs = {"pypi": get_reqs("dep" + req)} interpreter = "pythonX.Y" options = {"foo": "bar"} venv = json.dumps({ "metadata": "ok", "installed": {"pypi": {"dep": installed}}, "interpreter": "pythonX.Y", "options": {"foo": "bar"} }) resp = venvscache._select([venv], reqs, interpreter, uuid="", options=options) assert resp == expected @pytest.mark.parametrize("possible_venvs", [ [ (get_distrib(('dep', '3')), 'venv_best_fit'), ], [ (get_distrib(('dep1', '3'), ('dep2', '3')), 'venv_best_fit'), ], [ (get_distrib(('dep', '5')), 'venv_best_fit'), (get_distrib(('dep', '3')), 'venv_1'), ], [ (get_distrib(('dep1', '5'), ('dep2', '7')), 'venv_best_fit'), (get_distrib(('dep1', '3'), ('dep2', '6')), 'venv_1'), ], [ (get_distrib(('dep1', '3'), ('dep2', '9')), 'venv_1'), (get_distrib(('dep1', '5'), ('dep2', '7')), 'venv_best_fit'), ], [ (get_distrib(('dep1', '5'), ('dep2', '7')), 'venv_1'), (get_distrib(('dep1', '3'), ('dep2', '9')), 'venv_best_fit'), ], [ (get_distrib(('dep1', '3'), ('dep2', '9'), ('dep3', '4')), 'venv_best_fit'), (get_distrib(('dep1', '5'), ('dep2', '7'), ('dep3', '2')), 'venv_1'), ], [ (get_distrib(('dep2', '3'), ('dep1', '2'), ('dep3', '8')), 'venv_best_fit'), (get_distrib(('dep1', '7'), ('dep3', '5'), ('dep2', '2')), 'venv_1'), ], [ (get_distrib(('dep1', '3'), ('dep2', '2')), 'venv_1'), (get_distrib(('dep1', '4'), ('dep2', '2')), 'venv_2'), (get_distrib(('dep1', '5'), ('dep2', '7')), 'venv_best_fit'), (get_distrib(('dep1', '5'), ('dep2', '6')), 'venv_3'), ], [ ([parsing.VCSDependency('someurl')], 'venv_best_fit'), ], [ ([parsing.VCSDependency('someurl')] + get_distrib(('dep', '3')), 'venv_best_fit'), ], [ ([parsing.VCSDependency('someurl')] + get_distrib(('dep', '3')), 'venv_best_fit'), ([parsing.VCSDependency('someurl')] + get_distrib(('dep', '1')), 'venv_1'), ], ]) def test_best_fit(venvscache, possible_venvs): """Check the venv best fitting decissor.""" assert venvscache._select_better_fit(possible_venvs) == 'venv_best_fit' ================================================ FILE: tests/test_cache/test_remove.py ================================================ # Copyright 2015-2019 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades import json import os import time from threading import Thread from fades import cache def test_missing_file(tmp_file): venvscache = cache.VEnvsCache(tmp_file) venvscache.remove('missing/path') lines = venvscache._read_cache() assert lines == [] def test_missing_env_in_cache(tmp_file): venvscache = cache.VEnvsCache(tmp_file) options = {'foo': 'bar'} venvscache.store('installed', {'env_path': 'some/path'}, 'interpreter', options=options) lines = venvscache._read_cache() assert len(lines) == 1 venvscache.remove('some/path') lines = venvscache._read_cache() assert lines == [] def test_preserve_cache_data_ordering(tmp_file): venvscache = cache.VEnvsCache(tmp_file) # store 3 venvs options = {'foo': 'bar'} venvscache.store('installed1', {'env_path': 'path/env1'}, 'interpreter', options=options) venvscache.store('installed2', {'env_path': 'path/env2'}, 'interpreter', options=options) venvscache.store('installed3', {'env_path': 'path/env3'}, 'interpreter', options=options) venvscache.remove('path/env2') lines = venvscache._read_cache() assert len(lines) == 2 assert json.loads(lines[0]).get('metadata').get('env_path') == 'path/env1' assert json.loads(lines[1]).get('metadata').get('env_path') == 'path/env3' def test_lock_cache_for_remove(tmp_file): venvscache = cache.VEnvsCache(tmp_file) # store 3 venvs options = {'foo': 'bar'} venvscache.store('installed1', {'env_path': 'path/env1'}, 'interpreter', options=options) venvscache.store('installed2', {'env_path': 'path/env2'}, 'interpreter', options=options) venvscache.store('installed3', {'env_path': 'path/env3'}, 'interpreter', options=options) # patch _write_cache so it emulates a slow write during which # another process managed to modify the cache file before the # first process finished writing the modified cache data original_write_cache = venvscache._write_cache other_process = Thread(target=venvscache.remove, args=('path/env1',)) def slow_write_cache(*args, **kwargs): venvscache._write_cache = original_write_cache # start "other process" and wait a little to ensure it must wait # for the lock to be released other_process.start() time.sleep(0.01) original_write_cache(*args, **kwargs) venvscache._write_cache = slow_write_cache # just a sanity check assert not os.path.exists(venvscache.filepath + '.lock') # remove a virtualenv from the cache venvscache.remove('path/env2') other_process.join() # when cache file is properly locked both virtualenvs # will have been removed from the cache lines = venvscache._read_cache() assert len(lines) == 1 assert json.loads(lines[0]).get('metadata').get('env_path') == 'path/env3' assert not os.path.exists(venvscache.filepath + '.lock') ================================================ FILE: tests/test_cache/test_selection.py ================================================ # Copyright 2015-2019 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades import os import json import uuid from fades import helpers, parsing from tests import get_reqs def test_empty(venvscache): resp = venvscache._select([], {}, 'pythonX.Y', 'options') assert resp is None def test_nomatch_repo_dependency(venvscache): reqs = {"repoloco": get_reqs('dep == 5')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache._select([venv], reqs, interpreter, uuid='', options=options) assert resp is None def test_nomatch_pypi_dependency(venvscache): reqs = {'pypi': get_reqs('dep1 == 5')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep2': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache._select([venv], reqs, interpreter, uuid='', options=options) resp is None def test_nomatch_vcs_dependency(venvscache): reqs = {'vcs': [parsing.VCSDependency('someurl')]} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'vcs': {'otherurl': None}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache._select([venv], reqs, interpreter, uuid='', options=options) assert resp is None def test_nomatch_version(venvscache): reqs = {'pypi': get_reqs('dep == 5')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep': '7'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache._select([venv], reqs, interpreter, uuid='', options=options) assert resp is None def test_simple_pypi_match(venvscache): reqs = {'pypi': get_reqs('dep == 5')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache._select([venv], reqs, interpreter, uuid='', options=options) assert resp == 'foobar' def test_simple_vcs_match(venvscache): reqs = {'vcs': [parsing.VCSDependency('someurl')]} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'vcs': {'someurl': None}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp == 'foobar' def test_match_mixed_single(venvscache): reqs = {'vcs': [parsing.VCSDependency('someurl')], 'pypi': get_reqs('dep == 5')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv1 = json.dumps({ 'metadata': 'foobar1', 'installed': {'vcs': {'someurl': None}, 'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) venv2 = json.dumps({ 'metadata': 'foobar2', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) venv3 = json.dumps({ 'metadata': 'foobar3', 'installed': {'vcs': {'someurl': None}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select( [venv1, venv2, venv3], reqs, interpreter, uuid='', options=options) assert resp == 'foobar1' def test_match_mixed_multiple(venvscache): reqs = {'vcs': [parsing.VCSDependency('url1'), parsing.VCSDependency('url2')], 'pypi': get_reqs('dep1 == 5', 'dep2')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': { 'vcs': {'url1': None, 'url2': None}, 'pypi': {'dep1': '5', 'dep2': '7'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp == 'foobar' def test_match_noversion(venvscache): reqs = {'pypi': get_reqs('dep')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp == 'foobar' def test_middle_match(venvscache): reqs = {'pypi': get_reqs('dep == 5')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv1 = json.dumps({ 'metadata': 'venv1', 'installed': {'pypi': {'dep': '3'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) venv2 = json.dumps({ 'metadata': 'venv2', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) venv3 = json.dumps({ 'metadata': 'venv3', 'installed': {'pypi': {'dep': '7'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv1, venv2, venv3], reqs, interpreter, uuid='', options=options) assert resp == 'venv2' def test_multiple_match_bigger_version(venvscache): reqs = {'pypi': get_reqs('dep')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv1 = json.dumps({ 'metadata': 'venv1', 'installed': {'pypi': {'dep': '3'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) venv2 = json.dumps({ 'metadata': 'venv2', 'installed': {'pypi': {'dep': '7'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) venv3 = json.dumps({ 'metadata': 'venv3', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv1, venv2, venv3], reqs, interpreter, uuid='', options=options) # matches venv2 because it has the bigger version for 'dep' (even if it's not the # latest virtualenv created) assert resp == 'venv2' def test_multiple_deps_ok(venvscache): reqs = {'pypi': get_reqs('dep1 == 5', 'dep2 == 7')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep1': '5', 'dep2': '7'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp == 'foobar' def test_multiple_deps_just_one(venvscache): reqs = {'pypi': get_reqs('dep1 == 5', 'dep2 == 7')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep1': '5', 'dep2': '2'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp is None def test_not_too_crowded(venvscache): reqs = {'pypi': get_reqs('dep1')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep1': '5', 'dep2': '2'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp is None def test_same_quantity_different_deps(venvscache): reqs = {'pypi': get_reqs('dep1', 'dep2')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep1': '5', 'dep3': '2'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp is None def test_no_requirements_some_installed(venvscache): reqs = {} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep1': '5', 'dep3': '2'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp is None def test_no_requirements_empty_venv(venvscache): reqs = {} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp == "foobar" def test_simple_match_empty_options(venvscache): reqs = {'pypi': get_reqs('dep == 5')} interpreter = 'pythonX.Y' options = {} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp == "foobar" def test_no_match_due_to_options(venvscache): reqs = {'pypi': get_reqs('dep == 5')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv = json.dumps({ 'metadata': 'foobar', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {} }) resp = venvscache ._select([venv], reqs, interpreter, uuid='', options=options) assert resp is None def test_match_due_to_options(venvscache): reqs = {'pypi': get_reqs('dep == 5')} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv1 = json.dumps({ 'metadata': 'venv1', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {} }) venv2 = json.dumps({ 'metadata': 'venv2', 'installed': {'pypi': {'dep': '5'}}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv1, venv2], reqs, interpreter, uuid='', options=options) assert resp == "venv2" def test_no_deps_but_options(venvscache): reqs = {} interpreter = 'pythonX.Y' options = {'foo': 'bar'} venv1 = json.dumps({ 'metadata': 'venv1', 'installed': {}, 'interpreter': 'pythonX.Y', 'options': {} }) venv2 = json.dumps({ 'metadata': 'venv2', 'installed': {}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv1, venv2], reqs, interpreter, uuid='', options=options) assert resp == "venv2" def test_match_uuid(venvscache): venv_uuid = str(uuid.uuid4()) metadata = { 'env_path': os.path.join(helpers.get_basedir(), venv_uuid), } venv = json.dumps({ 'metadata': metadata, 'installed': {}, 'interpreter': 'pythonX.Y', 'options': {'foo': 'bar'} }) resp = venvscache ._select([venv], uuid=venv_uuid) assert resp == metadata ================================================ FILE: tests/test_cache/test_store.py ================================================ # Copyright 2015-2019 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades import json from fades import cache def test_missing_file(tmp_file): venvscache = cache.VEnvsCache(tmp_file) venvscache.store('installed', 'metadata', 'interpreter', 'options') with open(tmp_file, 'rt', encoding='utf8') as fh: data = json.loads(fh.readline()) assert 'timestamp' in data assert data['installed'], 'installed' assert data['metadata'], 'metadata' assert data['interpreter'], 'interpreter' assert data['options'], 'options' def test_with_previous_content(tmp_file): with open(tmp_file, 'wt', encoding='utf8') as fh: fh.write(json.dumps({'foo': 'bar'}) + '\n') venvscache = cache.VEnvsCache(tmp_file) venvscache.store('installed', 'metadata', 'interpreter', 'options') with open(tmp_file, 'rt', encoding='utf8') as fh: data = json.loads(fh.readline()) assert data, {'foo': 'bar'} data = json.loads(fh.readline()) assert 'timestamp' in data assert data['installed'], 'installed' assert data['metadata'], 'metadata' assert data['interpreter'], 'interpreter' assert data['options'], 'options' ================================================ FILE: tests/test_envbuilder.py ================================================ # Copyright 2015-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for the venv builder module.""" import os import shutil import tempfile import unittest from datetime import datetime, timedelta from unittest.mock import Mock, patch, call from packaging.requirements import Requirement import logassert from fades import FadesError, REPO_PYPI, REPO_VCS from fades import cache, envbuilder, parsing from venv import EnvBuilder def get_req(text): """Transform a text requirement into the Requirement object.""" return Requirement(text) class EnvCreationTestCase(unittest.TestCase): """Check all the new venv creation.""" class FakeManager: """A fake repo manager.""" def __init__(self): self.req_installed = [] self.really_installed = {} def install(self, dependency): self.req_installed.append(dependency) def get_version(self, dependency): return self.really_installed[dependency] class FailInstallManager(FakeManager): def install(self, dependency): raise Exception("Kapow!") def setUp(self): logassert.setup(self, 'fades.envbuilder') def test_create_simple(self): requested = { REPO_PYPI: [get_req('dep1 == v1'), get_req('dep2 == v2')] } interpreter = 'python3' is_current = True avoid_pip_upgrade = False options = {"venv_options": []} pip_options = [] with patch.object(envbuilder._FadesEnvBuilder, 'create_env') as mock_create: with patch.object(envbuilder, 'PipManager') as mock_mgr_c: mock_create.return_value = ('env_path', 'env_bin_path', 'pip_installed') mock_mgr_c.return_value = fake_manager = self.FakeManager() fake_manager.really_installed = {'dep1': 'v1', 'dep2': 'v2'} venv_data, installed = envbuilder.create_venv( requested, interpreter, is_current, options, pip_options, avoid_pip_upgrade) self.assertEqual(venv_data, { 'env_bin_path': 'env_bin_path', 'env_path': 'env_path', 'pip_installed': 'pip_installed', }) self.assertDictEqual(installed, { REPO_PYPI: { 'dep1': 'v1', 'dep2': 'v2', } }) expected_pipmanager_call = call( 'env_bin_path', pip_installed='pip_installed', options=[], avoid_pip_upgrade=avoid_pip_upgrade) self.assertEqual(mock_mgr_c.call_args, expected_pipmanager_call) def test_create_vcs(self): requested = { REPO_VCS: [parsing.VCSDependency("someurl")] } interpreter = 'python3' is_current = True avoid_pip_upgrade = False options = {"venv_options": []} pip_options = [] with patch.object(envbuilder._FadesEnvBuilder, 'create_env') as mock_create: with patch.object(envbuilder, 'PipManager') as mock_mgr_c: mock_create.return_value = ('env_path', 'env_bin_path', 'pip_installed') mock_mgr_c.return_value = self.FakeManager() venv_data, installed = envbuilder.create_venv( requested, interpreter, is_current, options, pip_options, avoid_pip_upgrade) self.assertEqual(venv_data, { 'env_bin_path': 'env_bin_path', 'env_path': 'env_path', 'pip_installed': 'pip_installed', }) self.assertDictEqual(installed, {REPO_VCS: {'someurl': None}}) def test_unknown_repo(self): requested = { 'unknown': {'dep': ''} } interpreter = 'python3' is_current = True avoid_pip_upgrade = False options = {"venv_options": []} pip_options = [] with patch.object(envbuilder._FadesEnvBuilder, 'create_env') as mock_create: with patch.object(envbuilder, 'PipManager') as mock_mgr_c: mock_create.return_value = ('env_path', 'env_bin_path', 'pip_installed') mock_mgr_c.return_value = self.FakeManager() envbuilder.create_venv( requested, interpreter, is_current, options, pip_options, avoid_pip_upgrade) self.assertLoggedWarning("Install from 'unknown' not implemented") def test_non_existing_dep(self): requested = { REPO_PYPI: [get_req('dep1 == 1000')] } interpreter = 'python3' is_current = True avoid_pip_upgrade = False options = {'venv_options': []} pip_options = [] with patch.object(envbuilder._FadesEnvBuilder, 'create_env') as mock_create: with patch.object(envbuilder, 'PipManager') as mock_mgr_c: mock_create.return_value = ('env_path', 'env_bin_path', 'pip_installed') mock_mgr_c.return_value = self.FailInstallManager() with patch.object(envbuilder, 'destroy_venv', spec=True) as mock_destroy: with self.assertRaises(FadesError) as cm: envbuilder.create_venv( requested, interpreter, is_current, options, pip_options, avoid_pip_upgrade) self.assertEqual(str(cm.exception), 'Dependency installation failed') mock_destroy.assert_called_once_with('env_path') self.assertLoggedDebug("Installation Step failed, removing virtual environment") def test_different_versions(self): requested = { REPO_PYPI: [get_req('dep1 == v1'), get_req('dep2 == v2')] } interpreter = 'python3' is_current = True avoid_pip_upgrade = False options = {"venv_options": []} pip_options = [] with patch.object(envbuilder._FadesEnvBuilder, 'create_env') as mock_create: with patch.object(envbuilder, 'PipManager') as mock_mgr_c: mock_create.return_value = ('env_path', 'env_bin_path', 'pip_installed') mock_mgr_c.return_value = fake_manager = self.FakeManager() fake_manager.really_installed = {'dep1': 'vX', 'dep2': 'v2'} _, installed = envbuilder.create_venv( requested, interpreter, is_current, options, pip_options, avoid_pip_upgrade) self.assertEqual(installed, { REPO_PYPI: { 'dep1': 'vX', 'dep2': 'v2', } }) def test_create_system_site_pkgs_venv(self): env_builder = envbuilder._FadesEnvBuilder() interpreter = 'python3' is_current = True options = {"venv_options": ['--system-site-packages']} with patch.object(EnvBuilder, 'create') as mock_create: env_builder.create_env(interpreter, is_current, options) self.assertTrue(env_builder.system_site_packages) self.assertTrue(mock_create.called) def test_create_pyvenv(self): env_builder = envbuilder._FadesEnvBuilder() interpreter = 'python3' is_current = True options = {"venv_options": []} with patch.object(EnvBuilder, 'create') as mock_create: env_builder.create_env(interpreter, is_current, options) self.assertFalse(env_builder.system_site_packages) self.assertTrue(mock_create.called) def test_create_virtual_environment(self): env_builder = envbuilder._FadesEnvBuilder() interpreter = 'pythonX.Y' is_current = False options = {"venv_options": []} with patch.object(envbuilder._FadesEnvBuilder, 'create_with_external_venv') as mock_create: env_builder.create_env(interpreter, is_current, options) mock_create.assert_called_with(interpreter, options['venv_options']) class EnvDestructionTestCase(unittest.TestCase): def test_destroy_venv(self): builder = envbuilder._FadesEnvBuilder() # make sure the virtualenv exists on disk options = {"venv_options": [], "pip-options": []} def fake_create(*_): """Fake venv create. This is for the test to avoid network usage on venv creation, but also create and set the fake dir. """ os.mkdir(fake_venv_path) builder.env_path = fake_venv_path fake_venv_path = tempfile.TemporaryDirectory().name builder.create_with_external_venv = fake_create builder.create_env('python', False, options=options) assert os.path.exists(builder.env_path) cache_mock = Mock() envbuilder.destroy_venv(builder.env_path, cache_mock) self.assertFalse(os.path.exists(builder.env_path)) cache_mock.remove.assert_called_with(builder.env_path) def test_destroy_venv_if_env_path_not_found(self): builder = envbuilder._FadesEnvBuilder() assert not os.path.exists(builder.env_path) cache_mock = Mock() envbuilder.destroy_venv(builder.env_path, cache_mock) self.assertFalse(os.path.exists(builder.env_path)) cache_mock.remove.assert_called_with(builder.env_path) class UsageManagerTestCase(unittest.TestCase): def setUp(self): temp_file_descriptor, self.tempfile = tempfile.mkstemp(prefix="test-temp-file") os.close(temp_file_descriptor) self.temp_folder = tempfile.mkdtemp() self.file_path = os.path.join(self.temp_folder, 'usage_stats') self.addCleanup(lambda: os.path.exists(self.tempfile) and os.remove(self.tempfile)) self.addCleanup(shutil.rmtree, self.temp_folder, ignore_errors=True) self.uuids = ['env1', 'env2', 'env3'] self.venvscache = cache.VEnvsCache(self.tempfile) for uuid in self.uuids: self.venvscache.store('', {'env_path': os.path.join(self.temp_folder, uuid)}, '', '') def get_usage_lines(self, manager): self.assertTrue(os.path.exists(self.file_path), msg="File usage exists") lines = [] for line in open(self.file_path).readlines(): uuid, d = line.split() d = manager._str_to_datetime(d) lines.append((uuid, d)) return lines def test_file_usage_dont_exists_then_it_is_created_and_initialized(self): self.assertFalse(os.path.exists(self.file_path), msg="First file doesn't exists") manager = envbuilder.UsageManager(self.file_path, self.venvscache) lines = self.get_usage_lines(manager) self.assertEqual(len(lines), len(self.uuids), msg="File have one line per venv") pending_uuids = self.uuids[:] for uuid, dt in lines: self.assertTrue(uuid in pending_uuids, msg="Every uuid is in file") pending_uuids.remove(uuid) def test_usage_record_is_recorded(self): manager = envbuilder.UsageManager(self.file_path, self.venvscache) lines = self.get_usage_lines(manager) self.assertEqual(len(lines), len(self.uuids), msg="File have one line per venv") venv = self.venvscache.get_venv(uuid=self.uuids[0]) manager.store_usage_stat(venv, self.venvscache) lines = self.get_usage_lines(manager) self.assertEqual(2, len([1 for u, d in lines if u == self.uuids[0]]), msg="Selected uuid is two times in file") def test_usage_file_is_compacted_when_though_no_venv_is_removed(self): old_date = datetime.now() new_date = old_date + timedelta(days=1) with patch('fades.envbuilder.datetime') as mock_datetime: mock_datetime.now.return_value = old_date mock_datetime.strptime.side_effect = lambda *args, **kw: datetime.strptime(*args, **kw) mock_datetime.strftime.side_effect = lambda *args, **kw: datetime.strftime(*args, **kw) manager = envbuilder.UsageManager(self.file_path, self.venvscache) lines = self.get_usage_lines(manager) for u, d in lines: self.assertEqual(old_date, d, msg="All records have the same date") venv = self.venvscache.get_venv(uuid=self.uuids[0]) manager.store_usage_stat(venv, self.venvscache) mock_datetime.now.return_value = new_date manager.store_usage_stat(venv, self.venvscache) lines = self.get_usage_lines(manager) self.assertEqual(len(self.uuids) + 2, len(lines)) manager.clean_unused_venvs(4) lines = self.get_usage_lines(manager) self.assertEqual(len(self.uuids), len(lines)) for u, d in lines: if u == self.uuids[0]: self.assertEqual(new_date, d, msg="Selected env have new date") else: self.assertEqual(old_date, d, msg="Others envs have old date") def test_executionerror_exception(self): env_builder = envbuilder._FadesEnvBuilder() interpreter = 'python3' is_current = False options = {"venv_options": []} with patch('fades.envbuilder.helpers.logged_exec') as mock_lexec: mock_lexec.side_effect = envbuilder.helpers.ExecutionError(1, 'cmd', ['stdout']) with self.assertRaises(FadesError) as cm: env_builder.create_env(interpreter, is_current, options) self.assertEqual(str(cm.exception), "Failed to run venv module externally") def test_general_error_exception(self): env_builder = envbuilder._FadesEnvBuilder() interpreter = 'python3' is_current = False options = {"venv_options": []} with patch('fades.envbuilder.helpers.logged_exec') as mock_lexec: mock_lexec.side_effect = Exception() with self.assertRaises(FadesError) as cm: env_builder.create_env(interpreter, is_current, options) self.assertEqual(str(cm.exception), "General error while running external venv") def test_when_a_venv_is_removed_it_is_removed_from_everywhere(self): old_date = datetime.now() new_date = old_date + timedelta(days=5) with patch('fades.envbuilder.datetime') as mock_datetime: mock_datetime.now.return_value = old_date mock_datetime.strptime.side_effect = lambda *args, **kw: datetime.strptime(*args, **kw) mock_datetime.strftime.side_effect = lambda *args, **kw: datetime.strftime(*args, **kw) manager = envbuilder.UsageManager(self.file_path, self.venvscache) lines = self.get_usage_lines(manager) for u, d in lines: self.assertEqual(old_date, d, msg="All records have the same date") venv = self.venvscache.get_venv(uuid=self.uuids[0]) manager.store_usage_stat(venv, self.venvscache) mock_datetime.now.return_value = new_date manager.store_usage_stat(venv, self.venvscache) lines = self.get_usage_lines(manager) self.assertEqual(len(self.uuids) + 2, len(lines)) with patch('fades.envbuilder.destroy_venv') as destroy_venv_mock: manager.clean_unused_venvs(4) lines = self.get_usage_lines(manager) self.assertEqual(1, len(lines), msg="Only one venv remains alive") uuid, d = lines[0] self.assertEqual(self.uuids[0], uuid, msg="The env who survive is the last used one.") # destroy_env and cache.remove was called for the others for uuid in self.uuids[1:]: env_path = self.venvscache.get_venv(uuid=uuid)['env_path'] destroy_venv_mock.assert_any_call(env_path, self.venvscache) ================================================ FILE: tests/test_file_options.py ================================================ # Copyright 2016 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for file_options.""" import argparse import unittest from configparser import ConfigParser from unittest.mock import patch from fades import file_options class OptionsFileTestCase(unittest.TestCase): """Check file_options.options_from_file().""" def setUp(self): self.argparser = argparse.ArgumentParser() self.argparser.add_argument self.argparser.add_argument('-f', '--foo', action='store_true') self.argparser.add_argument('-b', '--bar', action='store') self.argparser.add_argument('-d', '--dependency', action='append') self.argparser.add_argument('positional', nargs='?', default=None) def build_parser(self, args): config_parser = ConfigParser() config_parser['fades'] = args return config_parser @patch("fades.file_options.CONFIG_FILES", ('/foo/none', '/dev/null')) def test_no_config_files(self): args = self.argparser.parse_args([]) result = file_options.options_from_file(args) self.assertEqual(args, result) self.assertIsInstance(args, argparse.Namespace) @patch("fades.file_options.CONFIG_FILES", ('mock.ini',)) @patch("configparser.ConfigParser.items") def test_single_config_file_no_cli(self, mocked_parser): mocked_parser.return_value = [('foo', 'true'), ('bar', 'hux')] args = self.argparser.parse_args(['positional']) result = file_options.options_from_file(args) self.assertTrue(result.foo) self.assertEqual(result.bar, 'hux') self.assertIsInstance(args, argparse.Namespace) @patch("fades.file_options.CONFIG_FILES", ('mock.ini',)) @patch("configparser.ConfigParser.items") def test_single_config_file_with_cli(self, mocked_parser): mocked_parser.return_value = [('foo', 'false'), ('bar', 'hux'), ('no_in_cli', 'testing')] args = self.argparser.parse_args(['--foo', '--bar', 'other', 'positional']) result = file_options.options_from_file(args) self.assertTrue(result.foo) self.assertEqual(result.bar, 'other') self.assertEqual(result.no_in_cli, 'testing') self.assertIsInstance(args, argparse.Namespace) @patch("fades.file_options.CONFIG_FILES", ('mock.ini',)) @patch("configparser.ConfigParser.items") def test_single_config_file_with_mergeable(self, mocked_parser): mocked_parser.return_value = [('dependency', 'two')] args = self.argparser.parse_args( ['--foo', '--bar', 'other', '--dependency', 'one', 'positional']) result = file_options.options_from_file(args) self.assertTrue(result.foo) self.assertEqual(result.bar, 'other') self.assertEqual(result.dependency, ['one', 'two']) self.assertIsInstance(args, argparse.Namespace) @patch("fades.file_options.CONFIG_FILES", ('mock.ini',)) @patch("configparser.ConfigParser.items") def test_single_config_file_complex_mergeable(self, mocked_parser): mocked_parser.return_value = [('dependency', 'requests>=2.1,<2.8,!=2.6.5')] args = self.argparser.parse_args( ['--foo', '--bar', 'other', '--dependency', 'one', 'positional']) result = file_options.options_from_file(args) self.assertTrue(result.foo) self.assertEqual(result.bar, 'other') self.assertEqual(result.dependency, ['one', 'requests>=2.1,<2.8,!=2.6.5']) self.assertIsInstance(args, argparse.Namespace) @patch("fades.file_options.CONFIG_FILES", ('mock.ini', 'mock2.ini')) @patch("configparser.ConfigParser.items") def test_two_config_file_with_mergeable(self, mocked_parser): mocked_parser.side_effect = [ [('dependency', 'two')], [('dependency', 'three')], ] args = self.argparser.parse_args( ['--foo', '--bar', 'other', '--dependency', 'one', 'positional']) result = file_options.options_from_file(args) self.assertTrue(result.foo) self.assertEqual(result.bar, 'other') self.assertEqual(result.dependency, ['one', 'two', 'three']) self.assertIsInstance(args, argparse.Namespace) @patch("fades.file_options.CONFIG_FILES", ('mock.ini', 'mock2.ini')) @patch("configparser.ConfigParser.items") def test_two_config_file_with_booleans(self, mocked_parser): mocked_parser.side_effect = [ [('foo', 'true')], [('foo', 'false')], ] args = self.argparser.parse_args([]) result = file_options.options_from_file(args) self.assertFalse(result.foo) self.assertIsInstance(args, argparse.Namespace) @patch("fades.file_options.CONFIG_FILES", ('mock.ini', 'mock2.ini')) @patch("configparser.ConfigParser.items") def test_two_config_file_override_by_cli(self, mocked_parser): mocked_parser.side_effect = [ [('bar', 'no_this')], [('bar', 'no_this_b')], ] args = self.argparser.parse_args(['--bar', 'this']) result = file_options.options_from_file(args) self.assertEqual(result.bar, 'this') self.assertIsInstance(args, argparse.Namespace) @patch("fades.file_options.CONFIG_FILES", ('mock.ini', 'mock2.ini', 'mock3.ini')) @patch("configparser.ConfigParser.items") def test_three_config_file_override(self, mocked_parser): mocked_parser.side_effect = [ [('bar', 'no_this')], [('bar', 'neither_this')], [('bar', 'this')], ] args = self.argparser.parse_args([]) result = file_options.options_from_file(args) self.assertEqual(result.bar, 'this') self.assertIsInstance(args, argparse.Namespace) ================================================ FILE: tests/test_files/fades_as_part_of_other_word.py ================================================ import logging logger = logging.getLogger(__name__) def def_function(): """ the fades sweetly flower. foo==1.4 No te enfades con python3! bar>1.9 """ pass ================================================ FILE: tests/test_files/no_req.py ================================================ # Copyright 2014 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # """ Extended class from EnvBuilder to create a venv using a uuid4 id. NOTE: this class only work in the same python version that Fades is running. So, you don't need to have installed a virtualenv tool. For other python versions Fades needs a virtualenv tool installed. """ class FooClass(): """Create always a virtualenv.""" def foo(self): pass ================================================ FILE: tests/test_files/req_all.py ================================================ # Copyright 2014 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # """ Extended class from EnvBuilder to create a venv using a uuid4 id. NOTE: this class only work in the same python version that Fades is running. So, you don't need to have installed a virtualenv tool. For other python versions Fades needs a virtualenv tool installed. fades: foo==1.4 """ class FooClass(): """Create always a virtualenv. requirements for fades: bar>1.8.9 """ def __init__(self): pass def create(self, interpreter): """ Create a virtualenv using the virtualenv lib. fades: more>1.6 """ print("create") ================================================ FILE: tests/test_files/req_class.py ================================================ # Copyright 2014 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. """ Extended class from EnvBuilder to create a venv using a uuid4 id. NOTE: this class only work in the same python version that Fades is running. So, you don't need to have installed a virtualenv tool. For other python versions Fades needs a virtualenv tool installed. """ class FooClass(): """Foo class docstring. requirements for fades: foo bar """ def __init__(self): pass ================================================ FILE: tests/test_files/req_def.py ================================================ import logging logger = logging.getLogger(__name__) def def_function(): """Something. requirements for fades: foo bar !fades More info... """ pass ================================================ FILE: tests/test_files/req_mixed_backends.py ================================================ """ Bleh, groovy. fades: foo pypi::bar git+http://whatever vcs::anotherurl """ ================================================ FILE: tests/test_files/req_module.py ================================================ # Copyright 2014 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # """ Extended class from EnvBuilder to create a venv using a uuid4 id. NOTE: this class only work in the same python version that Fades is running. So, you don't need to have installed a virtualenv tool. For other python versions Fades needs a virtualenv tool installed. fades: foo bar """ class FooClass(): """Create always a virtualenv.""" def foo(self): pass ================================================ FILE: tests/test_files/req_module_2.py ================================================ # Copyright 2014 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # ''' Extended class from EnvBuilder to create a venv using a uuid4 id. NOTE: this class only work in the same python version that Fades is running. So, you don't need to have installed a virtualenv tool. For other python versions Fades needs a virtualenv tool installed. fades: foo bar ''' class FooClass(): """Create always a virtualenv.""" def foo(self): pass ================================================ FILE: tests/test_files/req_module_3.py ================================================ # Copyright 2014 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # "Extended class from EnvBuilder to create a venv using a uuid4 id." class FooClass(): """Create always a virtualenv.""" def foo(self): pass ================================================ FILE: tests/test_helpers.py ================================================ # Copyright 2015-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for functions in helpers.""" import io import json import os import sys import tempfile import unittest from http.server import HTTPStatus from unittest.mock import patch from urllib.error import HTTPError from urllib.request import Request import logassert import pytest from xdg import BaseDirectory from fades import helpers, parsing PATH_TO_EXAMPLES = "tests/examples/" class GetInterpreterVersionTestCase(unittest.TestCase): """Some tests for get_interpreter_version.""" def test_current_version(self): values = {None: ('/path/to/python1.0'), "/path/to/python": ('/path/to/python1.0')} def side_effect(arg=None): return values[arg] with patch.object(helpers, '_get_interpreter_info') as mock: mock.side_effect = side_effect interpreter, is_current = helpers.get_interpreter_version('/path/to/python') self.assertEqual(is_current, True) def test_other_version(self): values = {None: ('/path/to/python1.0'), "/path/to/python": ('/path/to/python9.8')} def side_effect(arg=None): return values[arg] with patch.object(helpers, '_get_interpreter_info') as mock: mock.side_effect = side_effect interpreter, is_current = helpers.get_interpreter_version('/path/to/python') self.assertEqual(is_current, False) def test_none_requested(self): values = {None: ('/path/to/python1.0'), "/path/to/python": ('/path/to/python9.8')} def side_effect(arg=None): return values[arg] with patch.object(helpers, '_get_interpreter_info') as mock: mock.side_effect = side_effect interpreter, is_current = helpers.get_interpreter_version(requested_interpreter=None) self.assertEqual(is_current, True) self.assertTrue(mock.call_count, 1) class GetInterpreterInfoTestCase(unittest.TestCase): """Some tests for _get_interpreter_info.""" def setUp(self): logassert.setup(self, 'fades.helpers') def test_none_requested(self): with patch.object(sys, 'version_info', (9, 8)), patch.object(sys, 'executable', '/path/to/python'): interpreter = helpers._get_interpreter_info(None) self.assertEqual(interpreter, '/path/to/python9.8') def test_requested_fullpath_nodigit(self): response = [('{"serial": 0,"path": "/path/to/python","minor": 8,"major": 9,"micro": 0,' '"releaselevel": "ultimate"}')] with patch.object(helpers, 'logged_exec', return_value=response): interpreter = helpers._get_interpreter_info('/path/to/python') self.assertEqual(interpreter, '/path/to/python9.8') def test_requested_fullpath_with_major(self): response = [('{"serial": 0,"path": "/path/to/python9","minor": 8,"major": 9,"micro": 0,' '"releaselevel": "ultimate"}')] with patch.object(helpers, 'logged_exec', return_value=response): interpreter = helpers._get_interpreter_info('/path/to/python9') self.assertEqual(interpreter, '/path/to/python9.8') def test_requested_fullpath_with_minor(self): response = [('{"serial": 0,"path": "/path/to/python9.8","minor": 8,"major": 9,"micro": 0,' '"releaselevel": "ultimate"}')] with patch.object(helpers, 'logged_exec', return_value=response): interpreter = helpers._get_interpreter_info('/path/to/python9.8') self.assertEqual(interpreter, '/path/to/python9.8') def test_requested_nodigit(self): response = [('{"serial": 0,"path": "/path/to/python","minor": 8,"major": 9,"micro": 0,' '"releaselevel": "ultimate"}')] with patch.object(helpers, 'logged_exec', return_value=response): interpreter = helpers._get_interpreter_info('python') self.assertEqual(interpreter, '/path/to/python9.8') def test_requested_with_major(self): response = [('{"serial": 0,"path": "/path/to/python9","minor": 8,"major": 9,"micro": 0,' '"releaselevel": "ultimate"}')] with patch.object(helpers, 'logged_exec', return_value=response): interpreter = helpers._get_interpreter_info('python9') self.assertEqual(interpreter, '/path/to/python9.8') def test_requested_with_minor(self): response = [('{"serial": 0,"path": "/path/to/python9.8","minor": 8,"major": 9,"micro": 0,' '"releaselevel": "ultimate"}')] with patch.object(helpers, 'logged_exec', return_value=response): interpreter = helpers._get_interpreter_info('python9.8') self.assertEqual(interpreter, '/path/to/python9.8') def test_requested_not_exists(self): side_effect = IOError("[Errno 2] No such file or directory: 'pythonME'") with patch('fades.helpers.logged_exec') as mock_lexec: mock_lexec.side_effect = side_effect with self.assertRaises(Exception): helpers._get_interpreter_info('pythonME') self.assertLoggedError("Error getting requested interpreter version:" " [Errno 2] No such file or directory: 'pythonME'") class GetLatestVersionNumberTestCase(unittest.TestCase): """Some tests for get_latest_version_number.""" def setUp(self): logassert.setup(self, 'fades.helpers') def test_get_version_correct(self): with open(os.path.join(PATH_TO_EXAMPLES, 'pypi_get_version_ok.json'), "rb") as fh: with patch('urllib.request.urlopen') as mock_urlopen: mock_urlopen.return_value = fh last_version = helpers.get_latest_version_number("some_package") mock_urlopen.assert_called_once_with(helpers.BASE_PYPI_URL.format(name="some_package")) self.assertEqual(last_version, '2.8.1') def test_get_version_wrong(self): with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.side_effect = HTTPError("url", 500, "mgs", {}, io.BytesIO()) mock_urlopen.return_value = mock_http_response self.assertRaises(Exception, helpers.get_latest_version_number, "some_package") self.assertLoggedWarning("Network error.") def test_get_version_fail(self): with open(os.path.join(PATH_TO_EXAMPLES, 'pypi_get_version_fail.json'), "rb") as fh: with patch('urllib.request.urlopen') as mock_urlopen: mock_urlopen.return_value = fh self.assertRaises(KeyError, helpers.get_latest_version_number, "some_package") self.assertLoggedError("Could not get the version of the package. Error:") class CheckPyPIUpdatesTestCase(unittest.TestCase): """Some tests for check_pypi_updates.""" def setUp(self): logassert.setup(self, 'fades.helpers') def test_check_pypi_updates_with_and_without_version(self): with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.side_effect = [b'{"info": {"version": "1.9"}}', b'{"info": {"version": "2.1"}}'] mock_urlopen.return_value = mock_http_response requested = parsing.parse_manual(["django==1.7.5", "requests"]) dependencies = helpers.check_pypi_updates(requested) dep_django = dependencies['pypi'][0] dep_request = dependencies['pypi'][1] self.assertLoggedInfo('There is a new version of django: 1.9') self.assertEqual(str(dep_request.specifier), "==2.1") self.assertEqual(str(dep_django.specifier), "==1.7.5") self.assertLoggedInfo("The latest version of 'requests' is 2.1 and will use it.") def test_check_pypi_updates_with_a_higher_version_of_a_package_simple(self): with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.side_effect = [b'{"info": {"version": "1.9"}}'] mock_urlopen.return_value = mock_http_response helpers.check_pypi_updates(parsing.parse_manual(["django==100.1.1"])) self.assertLoggedWarning( "The requested version for django is greater than latest found in PyPI: 1.9") def test_check_pypi_updates_with_a_higher_version_of_a_package_real_order(self): with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.side_effect = [b'{"info": {"version": "2.9"}}'] mock_urlopen.return_value = mock_http_response helpers.check_pypi_updates(parsing.parse_manual(["django==10.1"])) self.assertLoggedWarning( "The requested version for django is greater than latest found in PyPI: 2.9") def test_check_pypi_updates_with_the_latest_version_of_a_package(self): with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.side_effect = [b'{"info": {"version": "1.9"}}'] mock_urlopen.return_value = mock_http_response helpers.check_pypi_updates(parsing.parse_manual(["django==1.9"])) self.assertLoggedInfo( "The requested version for django is the latest one in PyPI: 1.9") class GetDirsTestCase(unittest.TestCase): """Utilities to get dir.""" _home = os.path.expanduser("~") def test_basedir_xdg(self): direct = helpers.get_basedir() self.assertEqual(direct, os.path.join(BaseDirectory.xdg_data_home, 'fades')) def _fake_snap_env_dir(self, direct): """Fake Snap's environment variable.""" os.environ[helpers.SNAP_BASEDIR_NAME] = direct self.addCleanup(os.environ.pop, helpers.SNAP_BASEDIR_NAME) def test_basedir_snap(self): with tempfile.TemporaryDirectory() as dirname: self._fake_snap_env_dir(dirname) direct = helpers.get_basedir() self.assertEqual(direct, os.path.join(dirname, 'data')) def test_basedir_default(self): with patch.object(helpers, "_get_basedirectory") as mock: mock.side_effect = ImportError() direct = helpers.get_basedir() self.assertEqual(direct, os.path.join(self._home, '.fades')) def test_basedir_xdg_nonexistant(self): with patch("xdg.BaseDirectory") as mock: with tempfile.TemporaryDirectory() as dirname: mock.xdg_data_home = dirname direct = helpers.get_basedir() self.assertTrue(os.path.exists(direct)) def test_basedir_snap_nonexistant(self): with tempfile.TemporaryDirectory() as dirname: self._fake_snap_env_dir(dirname) direct = helpers.get_basedir() self.assertTrue(os.path.exists(direct)) def test_confdir_xdg(self): direct = helpers.get_confdir() self.assertEqual(direct, os.path.join(BaseDirectory.xdg_config_home, 'fades')) def test_confdir_snap(self): with tempfile.TemporaryDirectory() as dirname: self._fake_snap_env_dir(dirname) direct = helpers.get_confdir() self.assertEqual(direct, os.path.join(dirname, 'config')) def test_confdir_default(self): with patch.object(helpers, "_get_basedirectory") as mock: mock.side_effect = ImportError() direct = helpers.get_confdir() self.assertEqual(direct, os.path.join(self._home, '.fades')) def test_confdir_xdg_nonexistant(self): with patch("xdg.BaseDirectory") as mock: with tempfile.TemporaryDirectory() as dirname: mock.xdg_config_home = dirname direct = helpers.get_confdir() self.assertTrue(os.path.exists(direct)) def test_confdir_snap_nonexistant(self): with tempfile.TemporaryDirectory() as dirname: self._fake_snap_env_dir(dirname) direct = helpers.get_confdir() self.assertTrue(os.path.exists(direct)) class CheckPackageExistenceTestCase(unittest.TestCase): """Test for check_pypi_exists.""" def setUp(self): logassert.setup(self, 'fades.helpers') def test_exists(self): deps = parsing.parse_manual(["foo"]) with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.status = HTTPStatus.OK mock_urlopen.return_value = mock_http_response exists = helpers.check_pypi_exists(deps) self.assertTrue(exists) self.assertLogged("exists in PyPI") def test_all_exists(self): dependencies = parsing.parse_manual(['foo', 'bar', 'baz']) with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.status = HTTPStatus.OK mock_urlopen.side_effect = [mock_http_response] * 3 exists = helpers.check_pypi_exists(dependencies) self.assertTrue(exists) self.assertLogged("exists in PyPI") def test_doesnt_exists(self): dependency = parsing.parse_manual(["foo"]) with patch('urllib.request.urlopen') as mock_urlopen: mock_http_error = HTTPError("url", HTTPStatus.NOT_FOUND, "mgs", {}, io.BytesIO()) mock_urlopen.side_effect = mock_http_error exists = helpers.check_pypi_exists(dependency) self.assertFalse(exists) self.assertLoggedError("foo doesn't exists in PyPI.") def test_one_doesnt_exists(self): dependencies = parsing.parse_manual(["foo", "bar"]) with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_error = HTTPError("url", HTTPStatus.NOT_FOUND, "mgs", {}, io.BytesIO()) mock_http_response.status = HTTPStatus.OK mock_urlopen.side_effect = [mock_http_response, mock_http_error] exists = helpers.check_pypi_exists(dependencies) self.assertFalse(exists) self.assertLoggedError("bar doesn't exists in PyPI.") def test_error_hitting_pypi(self): dependency = parsing.parse_manual(["foo"]) with self.assertRaises(Exception): with patch('urllib.request.urlopen') as mock_urlopen: mock_urlopen.side_effect = ValueError("cabum!!") helpers.check_pypi_exists(dependency) def test_status_code_error(self): dependency = parsing.parse_manual(["foo"]) with self.assertRaises(Exception): with patch('urllib.request.urlopen') as mock_urlopen: mock_http_error = HTTPError("url", 400, "mgs", {}, io.BytesIO()) mock_urlopen.side_effect = mock_http_error helpers.check_pypi_exists(dependency) def test_redirect_response(self): deps = parsing.parse_manual(["foo"]) with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.status = 302 # redirect mock_urlopen.return_value = mock_http_response exists = helpers.check_pypi_exists(deps) self.assertTrue(exists) self.assertLoggedWarning("Got a (unexpected) HTTP_STATUS") class ScriptDownloaderTestCase(unittest.TestCase): """Check the script downloader.""" def setUp(self): logassert.setup(self, 'fades.helpers') def test_external_public_function(self): test_url = "http://scripts.com/foobar.py" test_content = "test content of the remote script ññ" with patch('fades.helpers._ScriptDownloader') as mock_downloader_class: mock_downloader = mock_downloader_class() mock_downloader.get.return_value = test_content mock_downloader.name = 'mock downloader' filepath = helpers.download_remote_script(test_url) # plan to remove the downloaded content (so test remains clean) self.addCleanup(os.unlink, filepath) # checks mock_downloader_class.assert_called_with(test_url) self.assertLoggedInfo( "Downloading remote script from {!r}".format(test_url), repr(filepath), "(using 'mock downloader' downloader)") with open(filepath, "rt", encoding='utf8') as fh: self.assertEqual(fh.read(), test_content) def test_decide_linkode(self): url = "http://linkode.org/#02c5nESQBLEjgBRhUwJK74" downloader = helpers._ScriptDownloader(url) name = downloader._decide() self.assertEqual(name, 'linkode') def test_decide_pastebin(self): url = "https://pastebin.com/sZGwz7SL" downloader = helpers._ScriptDownloader(url) name = downloader._decide() self.assertEqual(name, 'pastebin') def test_decide_gist(self): url = "https://gist.github.com/facundobatista/6ff4f75760a9acc35e68bae8c1d7da1c" downloader = helpers._ScriptDownloader(url) name = downloader._decide() self.assertEqual(name, 'gist') def test_downloader_raw(self): test_url = "http://scripts.com/foobar.py" raw_service_response = b"test content of the remote script" downloader = helpers._ScriptDownloader(test_url) with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.return_value = raw_service_response mock_urlopen.return_value = mock_http_response mock_http_response.geturl.return_value = test_url content = downloader.get() # check urlopen was called with the proper url, and passing correct headers headers = { 'Accept': 'text/plain', 'User-agent': helpers._ScriptDownloader.USER_AGENT, } (call,) = mock_urlopen.mock_calls (called_request,) = call[1] self.assertIsInstance(called_request, Request) self.assertEqual(called_request.full_url, test_url) self.assertEqual(called_request.headers, headers) self.assertEqual(content, raw_service_response.decode("utf8")) def test_downloader_linkode(self): test_url = "http://linkode.org/#02c5nESQBLEjgBRhUwJK74" test_content = "test content of the remote script áéíóú" raw_service_response = json.dumps({ 'content': test_content, 'morestuff': 'whocares', }).encode("utf8") downloader = helpers._ScriptDownloader(test_url) with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.return_value = raw_service_response mock_urlopen.return_value = mock_http_response content = downloader.get() # check urlopen was called with the proper url, and passing correct headers headers = { 'Accept': 'application/json', 'User-agent': helpers._ScriptDownloader.USER_AGENT, } (call,) = mock_urlopen.mock_calls (called_request,) = call[1] self.assertIsInstance(called_request, Request) self.assertEqual( called_request.full_url, "https://linkode.org/api/1/linkodes/02c5nESQBLEjgBRhUwJK74") self.assertEqual(called_request.headers, headers) self.assertEqual(content, test_content) def test_downloader_pastebin(self): test_url = "http://pastebin.com/sZGwz7SL" real_url = "https://pastebin.com/raw/sZGwz7SL" test_content = "test content of the remote script áéíóú" raw_service_response = test_content.encode("utf8") downloader = helpers._ScriptDownloader(test_url) with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.return_value = raw_service_response mock_urlopen.return_value = mock_http_response mock_http_response.geturl.return_value = real_url content = downloader.get() # check urlopen was called with the proper url, and passing correct headers headers = { 'Accept': 'text/plain', 'User-agent': helpers._ScriptDownloader.USER_AGENT, } (call,) = mock_urlopen.mock_calls (called_request,) = call[1] self.assertIsInstance(called_request, Request) self.assertEqual(called_request.full_url, real_url) self.assertEqual(called_request.headers, headers) self.assertEqual(content, test_content) def test_downloader_gist(self): test_url = "http://gist.github.com/facundobatista/6ff4f75760a9acc35e68bae8c1d7da1c" real_url = "https://gist.github.com/facundobatista/6ff4f75760a9acc35e68bae8c1d7da1c/raw" test_content = "test content of the remote script áéíóú" raw_service_response = test_content.encode("utf8") downloader = helpers._ScriptDownloader(test_url) with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.return_value = raw_service_response mock_urlopen.return_value = mock_http_response mock_http_response.geturl.return_value = real_url content = downloader.get() # check urlopen was called with the proper url, and passing correct headers headers = { 'Accept': 'text/plain', 'User-agent': helpers._ScriptDownloader.USER_AGENT, } (call,) = mock_urlopen.mock_calls (called_request,) = call[1] self.assertIsInstance(called_request, Request) self.assertEqual(called_request.full_url, real_url) self.assertEqual(called_request.headers, headers) self.assertEqual(content, test_content) def test_downloader_raw_with_redirection(self): test_url = "http://bit.ly/will-redirect" final_url = "http://real-service.com/" raw_service_response = b"test content of the remote script" downloader = helpers._ScriptDownloader(test_url) response_contents = [ b"whatever; we don't care as we are redirectect", raw_service_response, ] with patch('urllib.request.urlopen') as mock_urlopen: with patch('http.client.HTTPResponse') as mock_http_response: mock_http_response.read.side_effect = lambda: response_contents.pop() mock_http_response.geturl.return_value = final_url mock_urlopen.return_value = mock_http_response content = downloader.get() # two calls, first to the service that will redirect us, second to the final one call1, call2 = mock_urlopen.mock_calls (called_request,) = call1[1] self.assertEqual(called_request.full_url, test_url) (called_request,) = call2[1] self.assertEqual(called_request.full_url, final_url) self.assertEqual(content, raw_service_response.decode("utf8")) self.assertLoggedInfo("Download redirect detect, now downloading from", final_url) def test_getbinpath_posix(tmp_path): realbin = tmp_path / "bin" realbin.mkdir() path = helpers.get_env_bin_path(tmp_path) assert path == realbin def test_getbinpath_windows(tmp_path): realbin = tmp_path / "Scripts" realbin.mkdir() path = helpers.get_env_bin_path(tmp_path) assert path == realbin def test_getbinpath_missing(tmp_path): with pytest.raises(ValueError): helpers.get_env_bin_path(tmp_path) ================================================ FILE: tests/test_infra.py ================================================ # Copyright 2017-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for infrastructure stuff.""" import io import logging from unittest.mock import patch import docutils.core import pydocstyle import pytest import rst2html5_ from flake8.api.legacy import get_style_guide from pyuca import Collator from tests import get_python_filepaths FLAKE8_ROOTS = ['fades', 'tests'] FLAKE8_OPTIONS = {'max_line_length': 99, 'select': ['E', 'W', 'F', 'C', 'N']} PEP257_ROOTS = ['fades'] # avoid seeing all DEBUG logs if the test fails for logger_name in ('flake8.plugins', 'flake8.api', 'flake8.checker', 'flake8.main'): logging.getLogger(logger_name).setLevel(logging.CRITICAL) def test_flake8_pytest(capsys): python_filepaths = get_python_filepaths(FLAKE8_ROOTS) style_guide = get_style_guide(**FLAKE8_OPTIONS) report = style_guide.check_files(python_filepaths) if report.total_errors != 0: out, _ = capsys.readouterr() pytest.fail(f"There are {report.total_errors} issues!\n{''.join(out)}") def test_pep257_pytest(): python_filepaths = get_python_filepaths(PEP257_ROOTS) to_ignore = { "D105", # Missing docstring in magic method "D107", # Missing docstring in __init__ } to_include = pydocstyle.violations.conventions.pep257 - to_ignore result = list(pydocstyle.check(python_filepaths, select=to_include)) assert len(result) == 0, "There are issues!\n" + '\n'.join(map(str, result)) def test_readme_sanity(): fake_stdout = io.StringIO() # just to ignore the output fake_stderr = io.StringIO() # will have content if there are problems with open('README.rst', 'rt', encoding='utf8') as fh: with patch('sys.stdout', fake_stdout): with patch('sys.stderr', fake_stderr): docutils.core.publish_file(source=fh, writer=rst2html5_.HTML5Writer()) errors = fake_stderr.getvalue() assert not bool(errors), "There are issues!\n" + errors def test_authors_ordering(): with open('AUTHORS', 'rt', encoding='utf8') as fh: authors = fh.readlines() ordered_authors = sorted(authors, key=Collator().sort_key) assert authors == ordered_authors ================================================ FILE: tests/test_logger.py ================================================ # Copyright 2018 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for logger related code.""" from fades.logger import set_up as log_set_up def test_salutes_info(logs): """Check saluting handler.""" logger = log_set_up(verbose=False, quiet=True) logger.warning("test foobar") assert "Hi! This is fades" in logs.info assert "test foobar" in logs.warning def test_salutes_once(logs): logger = log_set_up(verbose=False, quiet=False) logger.info("test foobar") assert "Hi! This is fades" in logs.info assert "test foobar" in logs.info # again, check this time it didn't salute, but original log message is ok logs.reset() logger.info("test barbarroja") assert "Hi! This is fades" not in logs.info assert "test barbarroja" in logs.info ================================================ FILE: tests/test_main.py ================================================ # Copyright 2015-2024 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for some code in main.""" import os import unittest from unittest.mock import patch from packaging.requirements import Requirement from fades import VERSION, FadesError, __version__, main, parsing, REPO_PYPI, REPO_VCS from tests import create_tempfile class VirtualenvCheckingTestCase(unittest.TestCase): """Tests for the virtualenv checker.""" def test_have_realprefix(self): resp = main.detect_inside_virtualenv('prefix', 'real_prefix', 'base_prefix') self.assertTrue(resp) def test_no_baseprefix(self): resp = main.detect_inside_virtualenv('prefix', None, None) self.assertFalse(resp) def test_prefix_is_baseprefix(self): resp = main.detect_inside_virtualenv('prefix', None, 'prefix') self.assertFalse(resp) def test_prefix_is_not_baseprefix(self): resp = main.detect_inside_virtualenv('prefix', None, 'other prefix') self.assertTrue(resp) class DepsGatheringTestCase(unittest.TestCase): """Tests for the gathering stage of consolidate_dependencies.""" def test_needs_ipython(self): d = main.consolidate_dependencies(needs_ipython=True, child_program=None, requirement_files=None, manual_dependencies=None) self.assertDictEqual(d, {'pypi': {Requirement('ipython')}}) def test_child_program(self): child_program = 'tests/test_files/req_module.py' d = main.consolidate_dependencies(needs_ipython=False, child_program=child_program, requirement_files=None, manual_dependencies=None) self.assertDictEqual(d, {'pypi': {Requirement('foo'), Requirement('bar')}}) def test_requirement_files(self): requirement_files = [create_tempfile(self, ['dep'])] d = main.consolidate_dependencies(needs_ipython=False, child_program=None, requirement_files=requirement_files, manual_dependencies=None) self.assertDictEqual(d, {'pypi': {Requirement('dep')}}) def test_manual_dependencies(self): manual_dependencies = ['dep'] d = main.consolidate_dependencies(needs_ipython=False, child_program=None, requirement_files=None, manual_dependencies=manual_dependencies) self.assertDictEqual(d, {'pypi': {Requirement('dep')}}) class DepsMergingTestCase(unittest.TestCase): """Tests for the merging stage of consolidate_dependencies.""" def test_two_different(self): requirement_files = [create_tempfile(self, ['1', '2'])] manual_dependencies = ['vcs::3', 'vcs::4'] d = main.consolidate_dependencies(needs_ipython=False, child_program=None, requirement_files=requirement_files, manual_dependencies=manual_dependencies) self.assertEqual(d, { 'pypi': {Requirement('1'), Requirement('2')}, 'vcs': {parsing.VCSDependency('3'), parsing.VCSDependency('4')} }) def test_two_same_repo(self): requirement_files = [create_tempfile(self, ['1', '2'])] manual_dependencies = ['3', '4'] d = main.consolidate_dependencies(needs_ipython=False, child_program=None, requirement_files=requirement_files, manual_dependencies=manual_dependencies) self.assertDictEqual(d, { 'pypi': {Requirement('1'), Requirement('2'), Requirement('3'), Requirement('4')} }) def test_complex_case(self): child_program = create_tempfile(self, ['"""fades:', '1', '2', '"""']) requirement_files = [create_tempfile(self, ['3', 'vcs::5'])] manual_dependencies = ['vcs::4', 'vcs::6'] d = main.consolidate_dependencies(needs_ipython=False, child_program=child_program, requirement_files=requirement_files, manual_dependencies=manual_dependencies) self.assertEqual(d, { 'pypi': {Requirement('1'), Requirement('2'), Requirement('3')}, 'vcs': {parsing.VCSDependency('5'), parsing.VCSDependency('4'), parsing.VCSDependency('6')} }) def test_one_duplicated(self): requirement_files = [create_tempfile(self, ['2', '2'])] manual_dependencies = None d = main.consolidate_dependencies(needs_ipython=False, child_program=None, requirement_files=requirement_files, manual_dependencies=manual_dependencies) self.assertDictEqual(d, { 'pypi': {Requirement('2')} }) def test_two_different_with_dups(self): requirement_files = [create_tempfile(self, ['1', '2', '2', '2'])] manual_dependencies = ['vcs::3', 'vcs::4', 'vcs::1', 'vcs::2'] d = main.consolidate_dependencies(needs_ipython=False, child_program=None, requirement_files=requirement_files, manual_dependencies=manual_dependencies) self.assertEqual(d, { 'pypi': {Requirement('1'), Requirement('2')}, 'vcs': {parsing.VCSDependency('1'), parsing.VCSDependency('2'), parsing.VCSDependency('3'), parsing.VCSDependency('4')} }) class MiscTestCase(unittest.TestCase): """Miscellaneous tests.""" def test_version_show(self): self.assertEqual( __version__, '.'.join([str(v) for v in VERSION]), ) class ChildProgramDeciderTestCase(unittest.TestCase): """Check how the child program is decided.""" def test_indicated_with_executable_flag(self): analyzable, child = main.decide_child_program(True, False, "foobar.py") self.assertIsNone(analyzable) self.assertEqual(child, "foobar.py") def test_no_child_at_all(self): analyzable, child = main.decide_child_program(False, False, None) self.assertIsNone(analyzable) self.assertIsNone(child) def test_normal_child_program(self): child_path = create_tempfile(self, "") analyzable, child = main.decide_child_program(False, False, child_path) self.assertEqual(analyzable, child_path) self.assertEqual(child, child_path) def test_normal_child_program_not_found(self): with self.assertRaises(FadesError): main.decide_child_program(False, False, 'does_not_exist.py') def test_normal_child_program_no_access(self): child_path = create_tempfile(self, "") os.chmod(child_path, 333) # Remove read permission. self.addCleanup(os.chmod, child_path, 644) with self.assertRaises(FadesError): main.decide_child_program(False, False, 'does_not_exist.py') def test_remote_child_program_simple(self): with patch('fades.helpers.download_remote_script') as mock: mock.return_value = "new_path_script" analyzable, child = main.decide_child_program( False, False, "http://scripts.com/foobar.py") mock.assert_called_with("http://scripts.com/foobar.py") # check that analyzable and child are the same, and that its content is the remote one self.assertEqual(analyzable, "new_path_script") self.assertEqual(child, "new_path_script") def test_remote_child_program_ssl(self): with patch('fades.helpers.download_remote_script') as mock: mock.return_value = "new_path_script" analyzable, child = main.decide_child_program( False, False, "https://scripts.com/foobar.py") mock.assert_called_with("https://scripts.com/foobar.py") # check that analyzable and child are the same, and that its content is the remote one self.assertEqual(analyzable, "new_path_script") self.assertEqual(child, "new_path_script") def test_indicated_with_executable_flag_with_relative_path(self): """Relative paths not allowed when using --exec.""" with self.assertRaises(FadesError): main.decide_child_program(True, False, os.path.join("path", "../foobar.py")) def test_indicated_with_executable_flag_with_absolute_path(self): """Absolute paths are allowed when using --exec.""" analyzable, child = main.decide_child_program(True, False, "/tmp/foo/bar.py") self.assertIsNone(analyzable) self.assertEqual(child, "/tmp/foo/bar.py") def test_module(self): child_path = 'foo.bar' analyzable, child = main.decide_child_program(False, True, child_path) self.assertIsNone(analyzable) self.assertEqual(child, child_path) # --------------------------------------- # autoimport tests def _autoimport_safe_call(*args, **kwargs): """Call the tested function and always remove the tempfile after the test.""" fpath = main.get_autoimport_scriptname(*args, **kwargs) with open(fpath, "rt", encoding='utf8') as fh: content = fh.read() os.unlink(fpath) return content def test_autoimport_simple(): """Simplest autoimport call.""" dependencies = { REPO_PYPI: {Requirement('mymod')}, } content = _autoimport_safe_call(dependencies, is_ipython=False) assert content.startswith(main.AUTOIMPORT_HEADER) assert main.AUTOIMPORT_MOD_IMPORTER.format(module='mymod') in content def test_autoimport_several_dependencies(): """Indicate several dependencies.""" dependencies = { REPO_PYPI: {Requirement('mymod1'), Requirement('mymod2')}, } content = _autoimport_safe_call(dependencies, is_ipython=False) assert content.startswith(main.AUTOIMPORT_HEADER) assert main.AUTOIMPORT_MOD_IMPORTER.format(module='mymod1') in content assert main.AUTOIMPORT_MOD_IMPORTER.format(module='mymod2') in content def test_autoimport_including_ipython(): """Call with ipython modifier.""" dependencies = { REPO_PYPI: { Requirement('mymod'), Requirement('ipython'), # this one is automatically added }, } content = _autoimport_safe_call(dependencies, is_ipython=True) assert main.AUTOIMPORT_HEADER not in content assert main.AUTOIMPORT_MOD_IMPORTER.format(module='mymod') in content assert 'ipython' not in content def test_autoimport_no_pypi_dep(): """Case with no pypi dependencies.""" dependencies = { REPO_PYPI: {Requirement('my_pypi_mod')}, REPO_VCS: {'my_vcs_dependency'}, } content = _autoimport_safe_call(dependencies, is_ipython=False) assert main.AUTOIMPORT_MOD_IMPORTER.format(module='my_pypi_mod') in content assert main.AUTOIMPORT_MOD_SKIPPING.format(dependency='my_vcs_dependency') in content def test_autoimport_importer_mod_ok(capsys): """Check the generated code to import a module when works fine.""" code = main.AUTOIMPORT_MOD_IMPORTER.format(module='time') # something from stdlib, always ok exec(code) assert capsys.readouterr().out == "::fades:: automatically imported 'time'\n" def test_autoimport_importer_mod_fail(capsys): """Check the generated code to import a module when works fine.""" code = main.AUTOIMPORT_MOD_IMPORTER.format(module='not_there_should_explode') exec(code) assert capsys.readouterr().out == "::fades:: FAILED to autoimport 'not_there_should_explode'\n" ================================================ FILE: tests/test_multiplatform.py ================================================ # Copyright 2016 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General # Public License version 3, as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. # If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for the helpers in multiplatform.""" import os import threading import time import unittest from fades.multiplatform import filelock class LockChecker(threading.Thread): """Helper to check the lock in other thread. The time.sleep() in the middle of the process is for time.time() granularity in different platforms to not mess our tests. """ def __init__(self, filepath): self.filepath = filepath self.in_lock = self.post_work = None self.middle_work = threading.Event() super().__init__() def run(self): with filelock(self.filepath): time.sleep(.01) self.in_lock = time.time() self.middle_work.wait() self.post_work = time.time() class LockCacheTestCase(unittest.TestCase): """Tests for the locking utility.""" def setUp(self): self.test_path = "test_filelock" def tearDown(self): if os.path.exists(self.test_path): os.remove(self.test_path) def wait(self, lock_checker, attr_name): """Wait at most a second for the LockChecker to end.""" for i in range(10): attr = getattr(lock_checker, attr_name) if attr is not None: # ended! return time.sleep(.3) self.fail("LC didnt end: %s" % (lock_checker,)) def test_lock_alone(self): lc = LockChecker(self.test_path) lc.start() lc.middle_work.set() self.wait(lc, 'post_work') def test_lock_intermixed(self): lc1 = LockChecker(self.test_path) lc1.start() self.wait(lc1, 'in_lock') lc2 = LockChecker(self.test_path) lc2.start() lc1.middle_work.set() self.wait(lc1, 'post_work') lc2.middle_work.set() self.wait(lc2, 'post_work') # check LC 2 waited to enter self.assertGreater(lc2.in_lock, lc1.post_work) def test_lock_exploding(self): # get the lock and explode in the middle (then ignore the blast) try: with filelock(self.test_path): raise ValueError("pumba") except ValueError: pass # get the lock again with filelock(self.test_path): pass ================================================ FILE: tests/test_parsing/test_docstrings.py ================================================ """Check the docstring parsing.""" import io from fades import parsing, REPO_PYPI, REPO_VCS from tests import get_reqs def test_empty(): parsed = parsing._parse_docstring( io.StringIO(""" """) ) assert parsed == {} def test_only_comment(): with open("tests/test_files/no_req.py") as f: parsed = parsing._parse_docstring(f) assert parsed == {} def test_req_in_module_docstring_triple_doublequoute(): with open("tests/test_files/req_module.py") as f: parsed = parsing._parse_docstring(f) assert parsed == {REPO_PYPI: get_reqs("foo", "bar")} def test_req_in_module_docstring_triple_singlequote(): with open("tests/test_files/req_module_2.py") as f: parsed = parsing._parse_docstring(f) assert parsed == {REPO_PYPI: get_reqs("foo", "bar")} def test_req_in_module_docstring_one_doublequote(): with open("tests/test_files/req_module_3.py") as f: parsed = parsing._parse_docstring(f) assert parsed == {} def test_req_in_class_docstring(): with open("tests/test_files/req_class.py") as f: parsed = parsing._parse_docstring(f) # no requirements found assert parsed == {} def test_req_in_def_docstring(): with open("tests/test_files/req_def.py") as f: parsed = parsing._parse_docstring(f) # no requirements found assert parsed == {} def test_req_in_multi_docstring(): with open("tests/test_files/req_all.py") as f: parsed = parsing._parse_docstring(f) # Only module requirements was found assert parsed == {REPO_PYPI: get_reqs("foo==1.4")} def test_fades_word_as_part_of_text(): with open("tests/test_files/fades_as_part_of_other_word.py") as f: parsed = parsing._parse_docstring(f) assert parsed == {} def test_mixed_backends(): with open("tests/test_files/req_mixed_backends.py") as f: parsed = parsing._parse_docstring(f) # Only module requirements was found assert parsed == { REPO_PYPI: get_reqs("foo", "bar"), REPO_VCS: [ parsing.VCSDependency("git+http://whatever"), parsing.VCSDependency("anotherurl"), ], } ================================================ FILE: tests/test_parsing/test_file.py ================================================ """Check the imports parsing.""" import io from logassert import Exact from fades import parsing, REPO_PYPI, REPO_VCS from tests import get_reqs def test_nocomment(): # note that we're testing the import at the beginning of the line, and # in also indented parsed = parsing._parse_content(io.StringIO("""import time import time from time import foo """)) assert parsed == {} def test_simple_default(): parsed = parsing._parse_content(io.StringIO(""" import time import foo # fades """)) assert parsed == {REPO_PYPI: get_reqs('foo')} def test_double(): parsed = parsing._parse_content(io.StringIO(""" import time # fades import foo # fades """)) assert parsed == { REPO_PYPI: get_reqs('time') + get_reqs('foo') } def test_version_same_default(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades == 3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo == 3.5') } def test_version_different(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades !=3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo !=3.5') } def test_version_same_no_spaces(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades==3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo ==3.5') } def test_version_same_two_spaces(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades == 3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo == 3.5') } def test_version_same_one_space_before(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades == 3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo == 3.5') } def test_version_same_two_space_before(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades == 3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo == 3.5') } def test_version_same_one_space_after(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades== 3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo == 3.5') } def test_version_same_two_space_after(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades== 3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo == 3.5') } def test_version_greater(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades > 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo > 2') } def test_version_greater_no_space(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades>2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >2') } def test_version_greater_no_space_default(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades>2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >2') } def test_version_greater_two_spaces(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades > 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo > 2') } def test_version_greater_one_space_after(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades> 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo > 2') } def test_version_greater_two_space_after(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades> 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo > 2') } def test_version_greater_one_space_before(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades> 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo > 2') } def test_version_greater_two_space_before(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades> 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo > 2') } def test_version_same_or_greater(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades >= 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >= 2') } def test_version_same_or_greater_no_spaces(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades>=2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >= 2') } def test_version_same_or_greater_one_space_before(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades >=2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >=2') } def test_version_same_or_greater_two_space_before(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades >=2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >=2') } def test_version_same_or_greater_one_space_after(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades>= 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >= 2') } def test_version_same_or_greater_two_space_after(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades>= 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >= 2') } def test_continuation_line(): parsed = parsing._parse_content(io.StringIO(""" import bar # fades > 2 import foo """)) assert parsed == { REPO_PYPI: get_reqs('foo > 2') } def test_from_import_simple(): parsed = parsing._parse_content(io.StringIO(""" from foo import bar # fades """)) assert parsed == { REPO_PYPI: get_reqs('foo') } def test_import(): parsed = parsing._parse_content(io.StringIO(""" import foo.bar # fades """)) assert parsed == { REPO_PYPI: get_reqs('foo') } def test_from_import_complex(): parsed = parsing._parse_content(io.StringIO(""" from baz.foo import bar # fades """)) assert parsed == { REPO_PYPI: get_reqs('baz') } def test_allow_other_comments(): parsed = parsing._parse_content(io.StringIO(""" from foo import * # NOQA # fades """)) assert parsed == { REPO_PYPI: get_reqs('foo') } def test_allow_other_comments_reverse_default(): parsed = parsing._parse_content(io.StringIO(""" from foo import * # fades # NOQA """)) assert parsed == { REPO_PYPI: get_reqs('foo') } def test_strange_import(logs): parsed = parsing._parse_content(io.StringIO(""" from foo bar import :( # fades """)) assert Exact( "Not understood import info: ['from', 'foo', 'bar', 'import', ':(']") in logs.debug assert parsed == {} def test_strange_fadesinfo(logs): parsed = parsing._parse_content(io.StringIO(""" import foo # fades broken::whatever """)) assert "Not understood fades repository: 'broken'" in logs.warning assert parsed == {} def test_strange_fadesinfo2(logs): parsed = parsing._parse_content(io.StringIO(""" import foo # fadesbroken """)) assert "Not understood fades info: 'fadesbroken'" in logs.warning assert parsed == {} def test_projectname_noversion_implicit(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades othername """)) assert parsed == { REPO_PYPI: get_reqs('othername') } def test_projectname_noversion_explicit(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades pypi::othername """)) assert parsed == { REPO_PYPI: get_reqs('othername') } def test_projectname_version_explicit(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades pypi::othername >= 3 """)) assert parsed == { REPO_PYPI: get_reqs('othername >= 3') } def test_projectname_version_nospace(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades othername==5 """)) assert parsed == { REPO_PYPI: get_reqs('othername==5') } def test_projectname_version_space(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades othername <5 """)) assert parsed == { REPO_PYPI: get_reqs('othername <5') } def test_projectname_pkgnamedb(): parsed = parsing._parse_content(io.StringIO(""" import bs4 # fades """)) assert parsed == { REPO_PYPI: get_reqs('beautifulsoup4') } def test_projectname_pkgnamedb_version(): parsed = parsing._parse_content(io.StringIO(""" import bs4 # fades >=5 """)) assert parsed == { REPO_PYPI: get_reqs('beautifulsoup4 >=5') } def test_projectname_pkgnamedb_othername_default(): parsed = parsing._parse_content(io.StringIO(""" import bs4 # fades othername """)) assert parsed == { REPO_PYPI: get_reqs('othername') } def test_projectname_pkgnamedb_version_othername(): parsed = parsing._parse_content(io.StringIO(""" import bs4 # fades othername >=5 """)) assert parsed == { REPO_PYPI: get_reqs('othername >=5') } def test_comma_separated_import(): parsed = parsing._parse_content(io.StringIO(""" from foo import bar, baz, qux # fades """)) assert parsed == { REPO_PYPI: get_reqs('foo') } def test_other_lines_with_fades_string(): parsed = parsing._parse_content(io.StringIO(""" import bar # fades print("screen fades to black") """)) assert parsed == { REPO_PYPI: get_reqs('bar') } def test_commented_line(logs): parsed = parsing._parse_content(io.StringIO(""" #import foo # fades """)) assert parsed == {} assert "Not understood fades" not in logs.warning def test_with_fades_commented_line(logs): parsed = parsing._parse_content(io.StringIO(""" #import foo # fades import bar # fades """)) assert parsed == { REPO_PYPI: get_reqs('bar') } assert "Not understood fades" not in logs.warning def test_with_commented_line(logs): parsed = parsing._parse_content(io.StringIO(""" import bar # fades # a commented line """)) assert parsed == { REPO_PYPI: get_reqs('bar') } assert "Not understood fades" not in logs.warning def test_vcs_explicit(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades vcs::superurl """)) assert parsed == { REPO_VCS: [parsing.VCSDependency('superurl')] } def test_vcs_implicit(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades http://www.whatever/project """)) assert parsed == { REPO_VCS: [parsing.VCSDependency('http://www.whatever/project')] } def test_mixed(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades vcs::superurl import bar # fades """)) assert parsed == { REPO_VCS: [parsing.VCSDependency('superurl')], REPO_PYPI: get_reqs('bar'), } def test_fades_and_hashtag_mentioned_in_code(): """Test the case where a string contains both: fades and hashtag (#) but is not an import. """ parsed = parsing._parse_content(io.StringIO(""" 'http://fades.readthedocs.io/en/release-7-0/readme.html#how-to-use-it' """)) assert parsed == {} def test_fades_and_hashtag_mentioned_in_code_mixed_with_imports(): parsed = parsing._parse_content(io.StringIO("""import requests # fades 'http://fades.readthedocs.io/en/release-7-0/readme.html#how-to-use-it' """)) assert parsed == { REPO_PYPI: get_reqs('requests') } def test_fades_user_strange_comment_with_hashtag_ignored(): parsed = parsing._parse_content(io.StringIO(""" import foo # fades==2 # Some comment with #hashtash """)) assert parsed == {} ================================================ FILE: tests/test_parsing/test_file_reqs.py ================================================ """Check the requirements parsing from a reqs.txt file.""" import os from fades import parsing, REPO_PYPI from tests import get_reqs def test_requirement_files(create_tmpfile): parsed = parsing.parse_reqfile(create_tmpfile(['foo'])) assert parsed == {REPO_PYPI: get_reqs('foo')} def test_nested_requirement_files(create_tmpfile): requirement_file = create_tmpfile(['foo']) requirement_file_nested = create_tmpfile( ['bar\n-r {}'.format(requirement_file)] ) parsed = parsing.parse_reqfile(requirement_file_nested) assert parsed == {REPO_PYPI: get_reqs('bar', 'foo')} def test_nested_requirement_files_invalid_format(logs, create_tmpfile): requirement_file_nested = create_tmpfile(['foo\n-r']) parsed = parsing.parse_reqfile(requirement_file_nested) assert parsed == {REPO_PYPI: get_reqs('foo')} assert "Invalid format to indicate a nested requirements file:" in logs.warning def test_nested_requirement_files_not_pwd(create_tmpfile): requirement_file = create_tmpfile(['foo']) fname = os.path.basename(requirement_file) requirement_file_nested = create_tmpfile( ['bar\n-r {}'.format(fname)]) parsed = parsing.parse_reqfile(requirement_file_nested) assert parsed, {REPO_PYPI: get_reqs('bar', 'foo')} def test_nested_requirement_files_first_line(create_tmpfile): requirement_file = create_tmpfile(['foo']) requirement_file_nested = create_tmpfile( ['\n-r {}\nbar'.format(requirement_file)]) parsed = parsing.parse_reqfile(requirement_file_nested) assert parsed == {REPO_PYPI: get_reqs('foo', 'bar')} def test_two_nested_requirement_files(create_tmpfile): requirement_file = create_tmpfile(['foo']) requirement_file_nested1 = create_tmpfile( ['bar\n-r {}'.format(requirement_file)]) requirement_file_nested2 = create_tmpfile( ['baz\n-r {}'.format(requirement_file_nested1)]) parsed = parsing.parse_reqfile(requirement_file_nested2) assert parsed == {REPO_PYPI: get_reqs('baz', 'bar', 'foo')} ================================================ FILE: tests/test_parsing/test_manual.py ================================================ """Tests for the check of the manual parsing.""" from fades import parsing, REPO_PYPI, REPO_VCS from tests import get_reqs def test_none(): parsed = parsing.parse_manual(None) assert parsed == {} def test_nothing(): parsed = parsing.parse_manual([]) assert parsed == {} def test_simple(): parsed = parsing.parse_manual(["pypi::foo"]) assert parsed == {REPO_PYPI: get_reqs("foo")} def test_simple_default_pypi(): parsed = parsing.parse_manual(["foo"]) assert parsed == {REPO_PYPI: get_reqs("foo")} def test_double(): parsed = parsing.parse_manual(["pypi::foo", "pypi::bar"]) assert parsed == {REPO_PYPI: get_reqs("foo", "bar")} def test_version(): parsed = parsing.parse_manual(["pypi::foo == 3.5"]) assert parsed == {REPO_PYPI: get_reqs("foo == 3.5")} def test_version_default(): parsed = parsing.parse_manual(["foo == 3.5"]) assert parsed == {REPO_PYPI: get_reqs("foo == 3.5")} def test_vcs_simple(): url = "git+git://server.com/etc" parsed = parsing.parse_manual(["vcs::" + url]) assert parsed == {REPO_VCS: [parsing.VCSDependency(url)]} def test_vcs_simple_default(): url = "git+git://server.com/etc" parsed = parsing.parse_manual([url]) assert parsed == {REPO_VCS: [parsing.VCSDependency(url)]} def test_mixed(): parsed = parsing.parse_manual(["pypi::foo", "vcs::git+git://server.com/etc"]) assert parsed == { REPO_PYPI: get_reqs("foo"), REPO_VCS: [parsing.VCSDependency("git+git://server.com/etc")], } ================================================ FILE: tests/test_parsing/test_reqs.py ================================================ """Check the requirements parsing.""" import io from logassert import Multiple from fades import parsing, REPO_PYPI, REPO_VCS from tests import get_reqs def test_empty(): parsed = parsing._parse_requirement(io.StringIO(""" """)) assert parsed == {} def test_simple(): parsed = parsing._parse_requirement(io.StringIO(""" pypi::foo """)) assert parsed == {REPO_PYPI: get_reqs('foo')} def test_simple_default(): parsed = parsing._parse_requirement(io.StringIO(""" foo """)) assert parsed == {REPO_PYPI: get_reqs('foo')} def test_double(): parsed = parsing._parse_requirement(io.StringIO(""" pypi::time foo """)) assert parsed == { REPO_PYPI: get_reqs('time') + get_reqs('foo') } def test_version_same(): parsed = parsing._parse_requirement(io.StringIO(""" pypi::foo == 3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo == 3.5') } def test_version_same_default(): parsed = parsing._parse_requirement(io.StringIO(""" foo == 3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo == 3.5') } def test_version_different(): parsed = parsing._parse_requirement(io.StringIO(""" foo !=3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo !=3.5') } def test_version_same_no_spaces(): parsed = parsing._parse_requirement(io.StringIO(""" foo==3.5 """)) assert parsed == { REPO_PYPI: get_reqs('foo ==3.5') } def test_version_greater_two_spaces(): parsed = parsing._parse_requirement(io.StringIO(""" foo > 2 """)) assert parsed == { REPO_PYPI: get_reqs('foo > 2') } def test_version_same_or_greater(): parsed = parsing._parse_requirement(io.StringIO(""" foo >=2 """)) assert parsed == { REPO_PYPI: get_reqs('foo >= 2') } def test_comments(): parsed = parsing._parse_requirement(io.StringIO(""" pypi::foo # some text # other text bar """)) assert parsed == { REPO_PYPI: get_reqs('foo') + get_reqs('bar') } def test_strange_repo(logs): parsed = parsing._parse_requirement(io.StringIO(""" unknown::foo """)) assert Multiple("Not understood fades repository", "unknown") in logs.warning assert parsed == {} def test_vcs_simple(): parsed = parsing._parse_requirement(io.StringIO(""" vcs::strangeurl """)) assert parsed == {REPO_VCS: [parsing.VCSDependency("strangeurl")]} def test_vcs_simple_default(): parsed = parsing._parse_requirement(io.StringIO(""" bzrhttp://server/bleh """)) assert parsed == {REPO_VCS: [parsing.VCSDependency("bzrhttp://server/bleh")]} def test_mixed(): parsed = parsing._parse_requirement(io.StringIO(""" vcs::strangeurl pypi::foo """)) assert parsed == { REPO_VCS: [parsing.VCSDependency("strangeurl")], REPO_PYPI: get_reqs('foo'), } ================================================ FILE: tests/test_parsing/test_vcs_dependency.py ================================================ """Check the VCSDependency.""" from fades import parsing def test_string_representation(): """This is particularly tested because it's the interface to be installed.""" dep = parsing.VCSDependency("testurl") assert str(dep), "testurl" def test_contains(): """This is particularly tested because it's how fulfilling is tested.""" dep1 = parsing.VCSDependency("testurl") assert dep1.specifier.contains(None) assert not dep1.specifier.contains("123") def test_equality(): dep1 = parsing.VCSDependency("testurl") dep2 = parsing.VCSDependency("testurl") dep3 = parsing.VCSDependency("otherurl") assert dep1 == dep2 assert not (dep1 == dep3) assert not (dep1 != dep2) assert dep1 != dep3 assert not (dep1 == 123) assert not (dep1 == "testurl") ================================================ FILE: tests/test_pipmanager.py ================================================ # Copyright 2015-2022 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for pip related code.""" import os import io import pytest from unittest.mock import patch, call from fades.pipmanager import PipManager from fades import pipmanager from fades import helpers BIN_PATH = "somepath" def test_get_parsing_ok_pytest(): mocked_stdout = [ "Name: foo", "Version: 2.0.0", "Location: ~/.local/share/fades/86cc492/lib/python3.4/site-packages", "Requires: ", ] mgr = PipManager(BIN_PATH, pip_installed=True) with patch.object(helpers, "logged_exec", return_value=mocked_stdout): version = mgr.get_version("foo") assert version, "2.0.0" def test_get_parsing_error(logs): mocked_stdout = [ "Name: foo", "Release: 2.0.0", "Location: ~/.local/share/fades/86cc492/lib/python3.4/site-packages", "Requires: ", ] mgr = PipManager(BIN_PATH, pip_installed=True) with patch.object(helpers, "logged_exec", return_value=mocked_stdout): version = mgr.get_version("foo") assert version == "" assert ( 'Fades is having problems getting the installed version. ' 'Run with -v or check the logs for details' ) in logs.error def test_real_case_levenshtein(): mocked_stdout = [ "Metadata-Version: 1.1", "Name: python-Levenshtein", "Version: 0.12.0", "License: GPL", ] mgr = PipManager(BIN_PATH, pip_installed=True) with patch.object(helpers, "logged_exec", return_value=mocked_stdout): version = mgr.get_version("foo") assert version == "0.12.0" def test_install(): mgr = PipManager(BIN_PATH, pip_installed=True) pip_path = os.path.join(BIN_PATH, "pip") with patch.object(helpers, "logged_exec") as mock: mgr.install("foo") # check it always upgrades pip, and then the proper install python_path = os.path.join(BIN_PATH, "python") c1 = call([python_path, "-m", "pip", "install", "pip", "--upgrade"]) c2 = call([pip_path, "install", "foo"]) assert mock.call_args_list == [c1, c2] def test_install_without_pip_upgrade(): mgr = PipManager(BIN_PATH, pip_installed=True, avoid_pip_upgrade=True) pip_path = os.path.join(BIN_PATH, "pip") with patch.object(helpers, "logged_exec") as mock: mgr.install("foo") mock.assert_called_with([pip_path, "install", "foo"]) def test_install_multiword_dependency(): mgr = PipManager(BIN_PATH, pip_installed=True) pip_path = os.path.join(BIN_PATH, "pip") with patch.object(helpers, "logged_exec") as mock: mgr.install("foo bar") mock.assert_called_with([pip_path, "install", "foo", "bar"]) def test_install_with_options(): mgr = PipManager(BIN_PATH, pip_installed=True, options=["--bar baz"]) pip_path = os.path.join(BIN_PATH, "pip") with patch.object(helpers, "logged_exec") as mock: mgr.install("foo") mock.assert_called_with([pip_path, "install", "foo", "--bar", "baz"]) def test_install_with_options_using_equal(): mgr = PipManager(BIN_PATH, pip_installed=True, options=["--bar=baz"]) pip_path = os.path.join(BIN_PATH, "pip") with patch.object(helpers, "logged_exec") as mock: mgr.install("foo") mock.assert_called_with([pip_path, "install", "foo", "--bar=baz"]) def test_install_raise_error(logs): mgr = PipManager(BIN_PATH, pip_installed=True) with patch.object(helpers, "logged_exec", side_effect=['ok', Exception("Kapow!")]): with pytest.raises(Exception): mgr.install("foo") assert "Error installing foo: Kapow!" in logs.error def test_install_without_pip(): mgr = PipManager(BIN_PATH, pip_installed=False) pip_path = os.path.join(BIN_PATH, "pip") with patch.object(helpers, "logged_exec") as mocked_exec: with patch.object(mgr, "_brute_force_install_pip") as mocked_install_pip: mgr.install("foo") assert mocked_install_pip.call_count == 1 mocked_exec.assert_called_with([pip_path, "install", "foo"]) def test_brute_force_install_pip_installer_exists(tmp_path): tmp_file = str(tmp_path / "hello.txt") mgr = PipManager(BIN_PATH, pip_installed=False) python_path = os.path.join(BIN_PATH, "python") # get the tempfile but leave it there to be found open(tmp_file, 'wt', encoding='utf8').close() mgr.pip_installer_fname = tmp_file with patch.object(helpers, "logged_exec") as mocked_exec: with patch.object(mgr, "_download_pip_installer") as download_installer: mgr._brute_force_install_pip() assert not download_installer.called mocked_exec.assert_called_with([python_path, mgr.pip_installer_fname, "-I"]) assert mgr.pip_installed def test_brute_force_install_pip_no_installer(tmp_path): tmp_file = str(tmp_path / "hello.txt") mgr = PipManager(BIN_PATH, pip_installed=False) python_path = os.path.join(BIN_PATH, "python") mgr.pip_installer_fname = tmp_file with patch.object(helpers, "logged_exec") as mocked_exec: with patch.object(mgr, "_download_pip_installer") as download_installer: mgr._brute_force_install_pip() download_installer.assert_called_once_with() mocked_exec.assert_called_with([python_path, mgr.pip_installer_fname, "-I"]) assert mgr.pip_installed def test_download_pip_installer(tmp_path): tmp_file = str(tmp_path / "hello.txt") mgr = PipManager(BIN_PATH, pip_installed=False) mgr.pip_installer_fname = tmp_file with patch("fades.pipmanager.request.urlopen", return_value=io.BytesIO(b"hola")) as urlopen: mgr._download_pip_installer() assert os.path.exists(mgr.pip_installer_fname) urlopen.assert_called_once_with(pipmanager.PIP_INSTALLER) def test_freeze(tmp_path): tmp_file = str(tmp_path / "reqtest.txt") # call and check pip was executed ok mgr = PipManager(BIN_PATH) with patch.object(helpers, "logged_exec") as mock: mock.return_value = ['moño>11', 'foo==1.2'] # "bad" order, on purpose mgr.freeze(tmp_file) pip_path = os.path.join(BIN_PATH, "pip") mock.assert_called_with([pip_path, "freeze", "--all", "--local"]) # check results were stored properly with open(tmp_file, 'rt', encoding='utf8') as fh: stored = fh.read() assert stored == 'foo==1.2\nmoño>11\n' ================================================ FILE: tests/test_pkgnamesdb.py ================================================ # Copyright 2020 Facundo Batista, Nicolás Demarchi # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # For further info, check https://github.com/PyAr/fades """Tests for the package names DB.""" from fades import pkgnamesdb def test_db_consistency(): """Ensure multiple DB entrypoints are consistent between them.""" assert len(pkgnamesdb.MODULE_TO_PACKAGE) == len(pkgnamesdb.PACKAGE_TO_MODULE)