Repository: python-poetry/poetry Branch: main Commit: 2c270050111b Files: 949 Total size: 3.5 MB Directory structure: gitextract_5_4n1xc3/ ├── .cirrus.yml ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── ---bug-report.yml │ │ ├── ---documentation.yml │ │ ├── ---feature-request.yml │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── actions/ │ │ ├── bootstrap-poetry/ │ │ │ └── action.yaml │ │ └── poetry-install/ │ │ └── action.yaml │ ├── dependabot.yml │ ├── scripts/ │ │ └── backport.sh │ └── workflows/ │ ├── .tests-matrix.yaml │ ├── backport.yaml │ ├── docs.yaml │ ├── lock-threads.yaml │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── CHANGELOG.md ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs/ │ ├── _index.md │ ├── basic-usage.md │ ├── building-extension-modules.md │ ├── cli.md │ ├── community.md │ ├── configuration.md │ ├── contributing.md │ ├── dependency-specification.md │ ├── faq.md │ ├── libraries.md │ ├── managing-dependencies.md │ ├── managing-environments.md │ ├── plugins.md │ ├── pre-commit-hooks.md │ ├── pyproject.md │ └── repositories.md ├── pyproject.toml ├── src/ │ └── poetry/ │ ├── __main__.py │ ├── __version__.py │ ├── config/ │ │ ├── __init__.py │ │ ├── config.py │ │ ├── config_source.py │ │ ├── dict_config_source.py │ │ ├── file_config_source.py │ │ └── source.py │ ├── console/ │ │ ├── __init__.py │ │ ├── application.py │ │ ├── command_loader.py │ │ ├── commands/ │ │ │ ├── __init__.py │ │ │ ├── about.py │ │ │ ├── add.py │ │ │ ├── build.py │ │ │ ├── cache/ │ │ │ │ ├── __init__.py │ │ │ │ ├── clear.py │ │ │ │ └── list.py │ │ │ ├── check.py │ │ │ ├── command.py │ │ │ ├── config.py │ │ │ ├── debug/ │ │ │ │ ├── __init__.py │ │ │ │ ├── info.py │ │ │ │ ├── resolve.py │ │ │ │ └── tags.py │ │ │ ├── env/ │ │ │ │ ├── __init__.py │ │ │ │ ├── activate.py │ │ │ │ ├── info.py │ │ │ │ ├── list.py │ │ │ │ ├── remove.py │ │ │ │ └── use.py │ │ │ ├── env_command.py │ │ │ ├── group_command.py │ │ │ ├── init.py │ │ │ ├── install.py │ │ │ ├── installer_command.py │ │ │ ├── lock.py │ │ │ ├── new.py │ │ │ ├── publish.py │ │ │ ├── python/ │ │ │ │ ├── __init__.py │ │ │ │ ├── install.py │ │ │ │ ├── list.py │ │ │ │ └── remove.py │ │ │ ├── remove.py │ │ │ ├── run.py │ │ │ ├── search.py │ │ │ ├── self/ │ │ │ │ ├── __init__.py │ │ │ │ ├── add.py │ │ │ │ ├── install.py │ │ │ │ ├── lock.py │ │ │ │ ├── remove.py │ │ │ │ ├── self_command.py │ │ │ │ ├── show/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── plugins.py │ │ │ │ ├── sync.py │ │ │ │ └── update.py │ │ │ ├── show.py │ │ │ ├── source/ │ │ │ │ ├── __init__.py │ │ │ │ ├── add.py │ │ │ │ ├── remove.py │ │ │ │ └── show.py │ │ │ ├── sync.py │ │ │ ├── update.py │ │ │ └── version.py │ │ ├── events/ │ │ │ ├── __init__.py │ │ │ └── console_events.py │ │ ├── exceptions.py │ │ └── logging/ │ │ ├── __init__.py │ │ ├── filters.py │ │ ├── formatters/ │ │ │ ├── __init__.py │ │ │ ├── builder_formatter.py │ │ │ └── formatter.py │ │ ├── io_formatter.py │ │ └── io_handler.py │ ├── exceptions.py │ ├── factory.py │ ├── inspection/ │ │ ├── __init__.py │ │ ├── info.py │ │ └── lazy_wheel.py │ ├── installation/ │ │ ├── __init__.py │ │ ├── chef.py │ │ ├── chooser.py │ │ ├── executor.py │ │ ├── installer.py │ │ ├── operations/ │ │ │ ├── __init__.py │ │ │ ├── install.py │ │ │ ├── operation.py │ │ │ ├── uninstall.py │ │ │ └── update.py │ │ └── wheel_installer.py │ ├── json/ │ │ ├── __init__.py │ │ └── schemas/ │ │ └── poetry.json │ ├── layouts/ │ │ ├── __init__.py │ │ ├── layout.py │ │ └── src.py │ ├── locations.py │ ├── masonry/ │ │ ├── __init__.py │ │ ├── api.py │ │ └── builders/ │ │ ├── __init__.py │ │ └── editable.py │ ├── mixology/ │ │ ├── __init__.py │ │ ├── assignment.py │ │ ├── failure.py │ │ ├── incompatibility.py │ │ ├── incompatibility_cause.py │ │ ├── partial_solution.py │ │ ├── result.py │ │ ├── set_relation.py │ │ ├── term.py │ │ └── version_solver.py │ ├── packages/ │ │ ├── __init__.py │ │ ├── dependency_package.py │ │ ├── direct_origin.py │ │ ├── locker.py │ │ ├── package_collection.py │ │ └── transitive_package_info.py │ ├── plugins/ │ │ ├── __init__.py │ │ ├── application_plugin.py │ │ ├── base_plugin.py │ │ ├── plugin.py │ │ └── plugin_manager.py │ ├── poetry.py │ ├── publishing/ │ │ ├── __init__.py │ │ ├── hash_manager.py │ │ ├── publisher.py │ │ └── uploader.py │ ├── puzzle/ │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── provider.py │ │ ├── solver.py │ │ └── transaction.py │ ├── py.typed │ ├── pyproject/ │ │ ├── __init__.py │ │ └── toml.py │ ├── repositories/ │ │ ├── __init__.py │ │ ├── abstract_repository.py │ │ ├── cached_repository.py │ │ ├── exceptions.py │ │ ├── http_repository.py │ │ ├── installed_repository.py │ │ ├── legacy_repository.py │ │ ├── link_sources/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── html.py │ │ │ └── json.py │ │ ├── lockfile_repository.py │ │ ├── parsers/ │ │ │ ├── __init__.py │ │ │ ├── html_page_parser.py │ │ │ └── pypi_search_parser.py │ │ ├── pypi_repository.py │ │ ├── repository.py │ │ ├── repository_pool.py │ │ └── single_page_repository.py │ ├── toml/ │ │ ├── __init__.py │ │ ├── exceptions.py │ │ └── file.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── _compat.py │ │ ├── authenticator.py │ │ ├── cache.py │ │ ├── constants.py │ │ ├── dependency_specification.py │ │ ├── env/ │ │ │ ├── __init__.py │ │ │ ├── base_env.py │ │ │ ├── env_manager.py │ │ │ ├── exceptions.py │ │ │ ├── generic_env.py │ │ │ ├── mock_env.py │ │ │ ├── null_env.py │ │ │ ├── python/ │ │ │ │ ├── __init__.py │ │ │ │ ├── exceptions.py │ │ │ │ ├── installer.py │ │ │ │ ├── manager.py │ │ │ │ └── providers.py │ │ │ ├── script_strings.py │ │ │ ├── site_packages.py │ │ │ ├── system_env.py │ │ │ └── virtual_env.py │ │ ├── extras.py │ │ ├── helpers.py │ │ ├── isolated_build.py │ │ ├── log_utils.py │ │ ├── password_manager.py │ │ ├── patterns.py │ │ ├── pip.py │ │ ├── threading.py │ │ └── wheel.py │ ├── vcs/ │ │ ├── __init__.py │ │ └── git/ │ │ ├── __init__.py │ │ ├── backend.py │ │ └── system.py │ └── version/ │ ├── __init__.py │ └── version_selector.py └── tests/ ├── __init__.py ├── config/ │ ├── __init__.py │ ├── test_config.py │ ├── test_config_source.py │ ├── test_dict_config_source.py │ ├── test_file_config_source.py │ └── test_source.py ├── conftest.py ├── console/ │ ├── __init__.py │ ├── commands/ │ │ ├── __init__.py │ │ ├── cache/ │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── test_clear.py │ │ │ └── test_list.py │ │ ├── conftest.py │ │ ├── debug/ │ │ │ ├── __init__.py │ │ │ ├── test_info.py │ │ │ └── test_resolve.py │ │ ├── env/ │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── helpers.py │ │ │ ├── test_activate.py │ │ │ ├── test_info.py │ │ │ ├── test_list.py │ │ │ ├── test_remove.py │ │ │ └── test_use.py │ │ ├── python/ │ │ │ ├── __init__.py │ │ │ ├── test_python_install.py │ │ │ ├── test_python_list.py │ │ │ └── test_python_remove.py │ │ ├── self/ │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── fixtures/ │ │ │ │ └── poetry-1.0.5-darwin.sha256sum │ │ │ ├── test_add_plugins.py │ │ │ ├── test_install.py │ │ │ ├── test_remove_plugins.py │ │ │ ├── test_self_command.py │ │ │ ├── test_show.py │ │ │ ├── test_show_plugins.py │ │ │ ├── test_sync.py │ │ │ ├── test_update.py │ │ │ └── utils.py │ │ ├── source/ │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── test_add.py │ │ │ ├── test_remove.py │ │ │ └── test_show.py │ │ ├── test_about.py │ │ ├── test_add.py │ │ ├── test_build.py │ │ ├── test_check.py │ │ ├── test_config.py │ │ ├── test_init.py │ │ ├── test_install.py │ │ ├── test_lock.py │ │ ├── test_new.py │ │ ├── test_publish.py │ │ ├── test_remove.py │ │ ├── test_run.py │ │ ├── test_search.py │ │ ├── test_show.py │ │ ├── test_sync.py │ │ ├── test_update.py │ │ └── test_version.py │ ├── conftest.py │ ├── logging/ │ │ ├── __init__.py │ │ ├── formatters/ │ │ │ ├── __init__.py │ │ │ └── test_builder_formatter.py │ │ └── test_io_formatter.py │ ├── test_application.py │ ├── test_application_command_not_found.py │ ├── test_application_global_options.py │ ├── test_application_removed_commands.py │ ├── test_exceptions_console_message.py │ └── test_exections_poetry_runtime_error.py ├── fixtures/ │ ├── bad_scripts_project/ │ │ ├── no_colon/ │ │ │ ├── README.rst │ │ │ ├── pyproject.toml │ │ │ └── simple_project/ │ │ │ └── __init__.py │ │ └── too_many_colon/ │ │ ├── README.rst │ │ ├── pyproject.toml │ │ └── simple_project/ │ │ └── __init__.py │ ├── build_constraints/ │ │ └── pyproject.toml │ ├── build_constraints_empty/ │ │ └── pyproject.toml │ ├── build_system_requires_not_available/ │ │ ├── README.rst │ │ ├── pyproject.toml │ │ └── simple_project/ │ │ └── __init__.py │ ├── build_systems/ │ │ ├── core_from_git/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── simple_project/ │ │ │ └── __init__.py │ │ ├── core_in_range/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── simple_project/ │ │ │ └── __init__.py │ │ ├── core_not_in_range/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── simple_project/ │ │ │ └── __init__.py │ │ ├── has_build_script/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── simple_project/ │ │ │ └── __init__.py │ │ ├── multiple_build_deps/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── simple_project/ │ │ │ └── __init__.py │ │ ├── no_build_backend/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── simple_project/ │ │ │ └── __init__.py │ │ ├── no_build_system/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── simple_project/ │ │ │ └── __init__.py │ │ └── no_core/ │ │ ├── README.md │ │ ├── pyproject.toml │ │ └── simple_project/ │ │ └── __init__.py │ ├── complete.toml │ ├── deleted_directory_dependency/ │ │ └── pyproject.toml │ ├── deleted_file_dependency/ │ │ └── pyproject.toml │ ├── directory/ │ │ ├── project_with_transitive_directory_dependencies/ │ │ │ ├── project_with_transitive_directory_dependencies/ │ │ │ │ └── __init__.py │ │ │ ├── pyproject.toml │ │ │ └── setup.py │ │ └── project_with_transitive_file_dependencies/ │ │ ├── inner-directory-project/ │ │ │ └── pyproject.toml │ │ ├── project_with_transitive_file_dependencies/ │ │ │ └── __init__.py │ │ └── pyproject.toml │ ├── distributions/ │ │ ├── demo-0.1.0-py2.py3-none-any.whl │ │ ├── demo-0.1.2-py2.py3-none-any.whl │ │ ├── demo_invalid_record-0.1.0-py2.py3-none-any.whl │ │ ├── demo_invalid_record2-0.1.0-py2.py3-none-any.whl │ │ ├── demo_metadata_version_23-0.1.0-py2.py3-none-any.whl │ │ ├── demo_metadata_version_24-0.1.0-py2.py3-none-any.whl │ │ ├── demo_metadata_version_299-0.1.0-py2.py3-none-any.whl │ │ ├── demo_metadata_version_unknown-0.1.0-py2.py3-none-any.whl │ │ └── demo_missing_dist_info-0.1.0-py2.py3-none-any.whl │ ├── excluded_subpackage/ │ │ ├── README.rst │ │ ├── example/ │ │ │ ├── __init__.py │ │ │ └── test/ │ │ │ ├── __init__.py │ │ │ └── excluded.py │ │ └── pyproject.toml │ ├── extended_project/ │ │ ├── README.rst │ │ ├── build.py │ │ ├── extended_project/ │ │ │ └── __init__.py │ │ └── pyproject.toml │ ├── extended_project_without_setup/ │ │ ├── README.rst │ │ ├── build.py │ │ ├── extended_project/ │ │ │ └── __init__.py │ │ └── pyproject.toml │ ├── extended_with_no_setup/ │ │ ├── README.md │ │ ├── build.py │ │ ├── extended/ │ │ │ ├── __init__.py │ │ │ └── extended.c │ │ └── pyproject.toml │ ├── git/ │ │ └── github.com/ │ │ ├── demo/ │ │ │ ├── demo/ │ │ │ │ ├── demo/ │ │ │ │ │ └── __init__.py │ │ │ │ └── pyproject.toml │ │ │ ├── namespace-package-one/ │ │ │ │ ├── namespace_package/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── one/ │ │ │ │ │ └── __init__.py │ │ │ │ └── setup.py │ │ │ ├── no-dependencies/ │ │ │ │ ├── demo/ │ │ │ │ │ └── __init__.py │ │ │ │ └── setup.py │ │ │ ├── no-version/ │ │ │ │ ├── demo/ │ │ │ │ │ └── __init__.py │ │ │ │ └── setup.py │ │ │ ├── non-canonical-name/ │ │ │ │ ├── demo/ │ │ │ │ │ └── __init__.py │ │ │ │ └── setup.py │ │ │ ├── poetry-plugin/ │ │ │ │ ├── poetry_plugin/ │ │ │ │ │ └── __init__.py │ │ │ │ └── pyproject.toml │ │ │ ├── poetry-plugin2/ │ │ │ │ └── subdir/ │ │ │ │ ├── poetry_plugin/ │ │ │ │ │ └── __init__.py │ │ │ │ └── pyproject.toml │ │ │ ├── prerelease/ │ │ │ │ ├── prerelease/ │ │ │ │ │ └── __init__.py │ │ │ │ └── pyproject.toml │ │ │ ├── pyproject-demo/ │ │ │ │ ├── demo/ │ │ │ │ │ └── __init__.py │ │ │ │ └── pyproject.toml │ │ │ └── subdirectories/ │ │ │ ├── one/ │ │ │ │ ├── one/ │ │ │ │ │ └── __init__.py │ │ │ │ └── pyproject.toml │ │ │ ├── one-copy/ │ │ │ │ ├── one/ │ │ │ │ │ └── __init__.py │ │ │ │ └── pyproject.toml │ │ │ └── two/ │ │ │ ├── pyproject.toml │ │ │ └── two/ │ │ │ └── __init__.py │ │ └── forked_demo/ │ │ └── subdirectories/ │ │ ├── one/ │ │ │ ├── one/ │ │ │ │ └── __init__.py │ │ │ └── pyproject.toml │ │ ├── one-copy/ │ │ │ ├── one/ │ │ │ │ └── __init__.py │ │ │ └── pyproject.toml │ │ └── two/ │ │ ├── pyproject.toml │ │ └── two/ │ │ └── __init__.py │ ├── incompatible_lock/ │ │ └── pyproject.toml │ ├── inspection/ │ │ ├── demo/ │ │ │ └── pyproject.toml │ │ ├── demo_no_setup_pkg_info_no_deps/ │ │ │ ├── PKG-INFO │ │ │ └── pyproject.toml │ │ ├── demo_no_setup_pkg_info_no_deps_dynamic/ │ │ │ ├── PKG-INFO │ │ │ └── pyproject.toml │ │ ├── demo_no_setup_pkg_info_no_deps_for_sure/ │ │ │ ├── PKG-INFO │ │ │ └── pyproject.toml │ │ ├── demo_only_requires_txt.egg-info/ │ │ │ ├── PKG-INFO │ │ │ └── requires.txt │ │ ├── demo_poetry_package/ │ │ │ └── pyproject.toml │ │ └── demo_with_obsolete_egg_info/ │ │ ├── demo-0.1.0.egg-info/ │ │ │ ├── PKG-INFO │ │ │ └── requires.txt │ │ └── pyproject.toml │ ├── invalid_lock/ │ │ └── pyproject.toml │ ├── invalid_pyproject/ │ │ └── pyproject.toml │ ├── invalid_pyproject_dep_name/ │ │ └── pyproject.toml │ ├── missing_directory_dependency/ │ │ └── pyproject.toml │ ├── missing_extra_directory_dependency/ │ │ └── pyproject.toml │ ├── missing_file_dependency/ │ │ └── pyproject.toml │ ├── nameless_pyproject/ │ │ └── pyproject.toml │ ├── no_name_project/ │ │ ├── README.rst │ │ └── pyproject.toml │ ├── non_package_mode/ │ │ └── pyproject.toml │ ├── old_lock/ │ │ └── pyproject.toml │ ├── old_lock_path_dependency/ │ │ ├── pyproject.toml │ │ └── quix/ │ │ └── pyproject.toml │ ├── outdated_lock/ │ │ └── pyproject.toml │ ├── private_pyproject/ │ │ └── pyproject.toml │ ├── project_plugins/ │ │ ├── my_application_plugin-1.0.dist-info/ │ │ │ ├── METADATA │ │ │ └── entry_points.txt │ │ ├── my_application_plugin-2.0.dist-info/ │ │ │ ├── METADATA │ │ │ └── entry_points.txt │ │ ├── my_other_plugin-1.0.dist-info/ │ │ │ ├── METADATA │ │ │ └── entry_points.txt │ │ ├── pyproject.toml │ │ ├── some_lib-1.0.dist-info/ │ │ │ └── METADATA │ │ └── some_lib-2.0.dist-info/ │ │ └── METADATA │ ├── project_with_extras/ │ │ ├── project_with_extras/ │ │ │ └── __init__.py │ │ └── pyproject.toml │ ├── project_with_git_dev_dependency/ │ │ └── pyproject.toml │ ├── project_with_local_dependencies/ │ │ └── pyproject.toml │ ├── project_with_multi_constraints_dependency/ │ │ ├── project/ │ │ │ └── __init__.py │ │ └── pyproject.toml │ ├── project_with_nested_local/ │ │ ├── bar/ │ │ │ └── pyproject.toml │ │ ├── foo/ │ │ │ └── pyproject.toml │ │ ├── pyproject.toml │ │ └── quix/ │ │ └── pyproject.toml │ ├── project_with_setup/ │ │ ├── my_package/ │ │ │ └── __init__.py │ │ └── setup.py │ ├── project_with_setup_calls_script/ │ │ ├── my_package/ │ │ │ └── __init__.py │ │ ├── pyproject.toml │ │ └── setup.py │ ├── pypi_reference/ │ │ └── pyproject.toml │ ├── sample_project/ │ │ ├── README.rst │ │ └── pyproject.toml │ ├── scripts/ │ │ ├── README.md │ │ ├── pyproject.toml │ │ └── scripts/ │ │ ├── __init__.py │ │ ├── check_argv0.py │ │ ├── exit_code.py │ │ └── return_code.py │ ├── self_version_not_ok/ │ │ └── pyproject.toml │ ├── self_version_not_ok_invalid_config/ │ │ └── pyproject.toml │ ├── self_version_ok/ │ │ └── pyproject.toml │ ├── simple_project/ │ │ ├── LICENSE │ │ ├── README.rst │ │ ├── dist/ │ │ │ └── simple_project-1.2.3-py2.py3-none-any.whl │ │ ├── pyproject.toml │ │ └── simple_project/ │ │ └── __init__.py │ ├── simple_project_legacy/ │ │ ├── LICENSE │ │ ├── README.rst │ │ ├── pyproject.toml │ │ └── simple_project/ │ │ └── __init__.py │ ├── up_to_date_lock/ │ │ └── pyproject.toml │ ├── up_to_date_lock_non_package/ │ │ └── pyproject.toml │ ├── wheel_with_no_requires_dist/ │ │ └── demo-0.1.0-py2.py3-none-any.whl │ ├── with-include/ │ │ ├── LICENSE │ │ ├── README.rst │ │ ├── extra_dir/ │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── sub_pkg/ │ │ │ │ ├── __init__.py │ │ │ │ └── vcs_excluded.txt │ │ │ └── vcs_excluded.txt │ │ ├── for_wheel_only/ │ │ │ └── __init__.py │ │ ├── my_module.py │ │ ├── notes.txt │ │ ├── package_with_include/ │ │ │ └── __init__.py │ │ ├── pyproject.toml │ │ ├── src/ │ │ │ └── src_package/ │ │ │ └── __init__.py │ │ └── tests/ │ │ └── __init__.py │ ├── with_conditional_path_deps/ │ │ ├── demo_one/ │ │ │ └── pyproject.toml │ │ ├── demo_two/ │ │ │ └── pyproject.toml │ │ └── pyproject.toml │ ├── with_explicit_pypi_and_other/ │ │ └── pyproject.toml │ ├── with_explicit_pypi_and_other_explicit/ │ │ └── pyproject.toml │ ├── with_explicit_pypi_no_other/ │ │ └── pyproject.toml │ ├── with_explicit_source/ │ │ └── pyproject.toml │ ├── with_local_config/ │ │ ├── README.rst │ │ ├── poetry.toml │ │ └── pyproject.toml │ ├── with_multiple_dist_dir/ │ │ ├── README.rst │ │ ├── dist/ │ │ │ └── simple_project-1.2.3-py2.py3-none-any.whl │ │ ├── other_dist/ │ │ │ └── dist/ │ │ │ └── simple_project-1.2.3-py2.py3-none-any.whl │ │ ├── pyproject.toml │ │ └── simple_project/ │ │ └── __init__.py │ ├── with_multiple_readme_files/ │ │ ├── README-1.rst │ │ ├── README-2.rst │ │ ├── my_package/ │ │ │ └── __init__.py │ │ └── pyproject.toml │ ├── with_multiple_sources/ │ │ └── pyproject.toml │ ├── with_multiple_sources_pypi/ │ │ └── pyproject.toml │ ├── with_multiple_supplemental_sources/ │ │ └── pyproject.toml │ ├── with_path_dependency/ │ │ ├── bazz/ │ │ │ └── pyproject.toml │ │ └── pyproject.toml │ ├── with_primary_source_explicit/ │ │ └── pyproject.toml │ ├── with_primary_source_implicit/ │ │ └── pyproject.toml │ ├── with_source/ │ │ ├── README.rst │ │ └── pyproject.toml │ └── with_supplemental_source/ │ └── pyproject.toml ├── helpers.py ├── inspection/ │ ├── __init__.py │ ├── test_info.py │ └── test_lazy_wheel.py ├── installation/ │ ├── __init__.py │ ├── conftest.py │ ├── fixtures/ │ │ ├── extras-with-dependencies.test │ │ ├── extras.test │ │ ├── install-no-dev.test │ │ ├── no-dependencies.test │ │ ├── remove.test │ │ ├── update-with-lock.test │ │ ├── update-with-locked-extras.test │ │ ├── with-conditional-dependency.test │ │ ├── with-conflicting-dependency-extras-root.test │ │ ├── with-conflicting-dependency-extras-transitive.test │ │ ├── with-dependencies-differing-extras.test │ │ ├── with-dependencies-extras.test │ │ ├── with-dependencies-nested-extras.test │ │ ├── with-dependencies.test │ │ ├── with-directory-dependency-poetry-transitive.test │ │ ├── with-directory-dependency-poetry.test │ │ ├── with-directory-dependency-setuptools.test │ │ ├── with-duplicate-dependencies-update.test │ │ ├── with-duplicate-dependencies.test │ │ ├── with-exclusive-extras.test │ │ ├── with-file-dependency-transitive.test │ │ ├── with-file-dependency.test │ │ ├── with-multiple-updates.test │ │ ├── with-optional-dependencies.test │ │ ├── with-platform-dependencies.test │ │ ├── with-prereleases.test │ │ ├── with-pypi-repository.test │ │ ├── with-python-versions.test │ │ ├── with-same-version-url-dependencies.test │ │ ├── with-self-referencing-extras-all-deep.test │ │ ├── with-self-referencing-extras-all-top.test │ │ ├── with-self-referencing-extras-b-markers.test │ │ ├── with-self-referencing-extras-deep.test │ │ ├── with-self-referencing-extras-download-deep.test │ │ ├── with-self-referencing-extras-download-top.test │ │ ├── with-self-referencing-extras-install-deep.test │ │ ├── with-self-referencing-extras-install-download-deep.test │ │ ├── with-self-referencing-extras-install-download-top.test │ │ ├── with-self-referencing-extras-install-top.test │ │ ├── with-self-referencing-extras-nested-deep.test │ │ ├── with-self-referencing-extras-nested-top.test │ │ ├── with-self-referencing-extras-top.test │ │ ├── with-sub-dependencies.test │ │ ├── with-url-dependency.test │ │ ├── with-vcs-dependency-with-extras.test │ │ ├── with-vcs-dependency-without-ref.test │ │ └── with-wheel-dependency-no-requires-dist.test │ ├── test_chef.py │ ├── test_chooser.py │ ├── test_chooser_errors.py │ ├── test_executor.py │ ├── test_installer.py │ └── test_wheel_installer.py ├── integration/ │ ├── __init__.py │ └── test_utils_vcs_git.py ├── json/ │ ├── __init__.py │ ├── fixtures/ │ │ ├── build_constraints.toml │ │ ├── self_invalid_plugin.toml │ │ ├── self_invalid_version.toml │ │ ├── self_valid.toml │ │ └── source/ │ │ ├── complete_invalid_priority.toml │ │ ├── complete_invalid_url.toml │ │ └── complete_valid.toml │ └── test_schema.py ├── masonry/ │ └── builders/ │ ├── __init__.py │ ├── fixtures/ │ │ └── excluded_subpackage/ │ │ ├── README.rst │ │ ├── example/ │ │ │ ├── __init__.py │ │ │ └── test/ │ │ │ ├── __init__.py │ │ │ └── excluded.py │ │ └── pyproject.toml │ └── test_editable_builder.py ├── mixology/ │ ├── __init__.py │ ├── helpers.py │ ├── test_incompatibility.py │ └── version_solver/ │ ├── __init__.py │ ├── conftest.py │ ├── test_backtracking.py │ ├── test_basic_graph.py │ ├── test_dependency_cache.py │ ├── test_python_constraint.py │ ├── test_unsolvable.py │ └── test_with_lock.py ├── packages/ │ ├── __init__.py │ ├── test_direct_origin.py │ ├── test_locker.py │ └── test_transitive_package_info.py ├── plugins/ │ ├── __init__.py │ └── test_plugin_manager.py ├── publishing/ │ ├── __init__.py │ ├── test_hash_manager.py │ ├── test_publisher.py │ └── test_uploader.py ├── puzzle/ │ ├── __init__.py │ ├── conftest.py │ ├── test_provider.py │ ├── test_solver.py │ ├── test_solver_internals.py │ └── test_transaction.py ├── pyproject/ │ ├── __init__.py │ ├── conftest.py │ ├── test_pyproject_toml.py │ └── test_pyproject_toml_file.py ├── repositories/ │ ├── __init__.py │ ├── conftest.py │ ├── fixtures/ │ │ ├── __init__.py │ │ ├── distribution_hashes.py │ │ ├── installed/ │ │ │ ├── lib/ │ │ │ │ └── python3.7/ │ │ │ │ └── site-packages/ │ │ │ │ ├── cleo-0.7.6.dist-info/ │ │ │ │ │ └── METADATA │ │ │ │ ├── directory_pep_610-1.2.3.dist-info/ │ │ │ │ │ ├── METADATA │ │ │ │ │ └── direct_url.json │ │ │ │ ├── editable-2.3.4.dist-info/ │ │ │ │ │ └── METADATA │ │ │ │ ├── editable-src-dir-2.3.4.dist-info/ │ │ │ │ │ └── METADATA │ │ │ │ ├── editable-src-dir.pth │ │ │ │ ├── editable-with-import-2.3.4.dist-info/ │ │ │ │ │ └── METADATA │ │ │ │ ├── editable-with-import.pth │ │ │ │ ├── editable.pth │ │ │ │ ├── editable_directory_pep_610-1.2.3.dist-info/ │ │ │ │ │ ├── METADATA │ │ │ │ │ └── direct_url.json │ │ │ │ ├── file_pep_610-1.2.3.dist-info/ │ │ │ │ │ ├── METADATA │ │ │ │ │ └── direct_url.json │ │ │ │ ├── foo-0.1.0-py3.8.egg │ │ │ │ ├── git_pep_610-1.2.3.dist-info/ │ │ │ │ │ ├── METADATA │ │ │ │ │ └── direct_url.json │ │ │ │ ├── git_pep_610_no_requested_version-1.2.3.dist-info/ │ │ │ │ │ ├── METADATA │ │ │ │ │ └── direct_url.json │ │ │ │ ├── git_pep_610_subdirectory-1.2.3.dist-info/ │ │ │ │ │ ├── METADATA │ │ │ │ │ └── direct_url.json │ │ │ │ ├── standard-1.2.3.dist-info/ │ │ │ │ │ └── METADATA │ │ │ │ ├── standard.pth │ │ │ │ └── url_pep_610-1.2.3.dist-info/ │ │ │ │ ├── METADATA │ │ │ │ └── direct_url.json │ │ │ ├── lib64/ │ │ │ │ └── python3.7/ │ │ │ │ └── site-packages/ │ │ │ │ ├── bender-2.0.5.dist-info/ │ │ │ │ │ └── METADATA │ │ │ │ ├── bender.pth │ │ │ │ └── lib64-2.3.4.dist-info/ │ │ │ │ └── METADATA │ │ │ ├── src/ │ │ │ │ ├── bender/ │ │ │ │ │ └── bender.egg-info/ │ │ │ │ │ └── PKG-INFO │ │ │ │ └── pendulum/ │ │ │ │ └── pendulum.egg-info/ │ │ │ │ ├── PKG-INFO │ │ │ │ └── requires.txt │ │ │ └── vendor/ │ │ │ └── py3.7/ │ │ │ └── attrs-19.3.0.dist-info/ │ │ │ └── METADATA │ │ ├── legacy/ │ │ │ ├── absolute.html │ │ │ ├── black.html │ │ │ ├── clikit.html │ │ │ ├── demo.html │ │ │ ├── discord-py.html │ │ │ ├── futures-partial-yank.html │ │ │ ├── futures.html │ │ │ ├── invalid-version.html │ │ │ ├── ipython.html │ │ │ ├── isort-metadata.html │ │ │ ├── isort.html │ │ │ ├── json/ │ │ │ │ ├── _readme │ │ │ │ ├── absolute.json │ │ │ │ ├── demo.json │ │ │ │ ├── invalid-version.json │ │ │ │ ├── isort-metadata.json │ │ │ │ ├── jupyter.json │ │ │ │ ├── poetry-test-py2-py3-metadata-merge.json │ │ │ │ ├── relative.json │ │ │ │ └── sqlalchemy-legacy.json │ │ │ ├── jupyter.html │ │ │ ├── missing-version.html │ │ │ ├── pastel.html │ │ │ ├── poetry-test-py2-py3-metadata-merge.html │ │ │ ├── pytest-with-extra-packages.html │ │ │ ├── pytest.html │ │ │ ├── python-language-server.html │ │ │ ├── pyyaml.html │ │ │ ├── relative.html │ │ │ ├── sqlalchemy-legacy.html │ │ │ └── tomlkit.html │ │ ├── legacy.py │ │ ├── pypi.org/ │ │ │ ├── dists/ │ │ │ │ ├── mocked/ │ │ │ │ │ ├── poetry_test_py2_py3_metadata_merge-0.1.0-py2-none-any.whl │ │ │ │ │ └── poetry_test_py2_py3_metadata_merge-0.1.0-py3-none-any.whl │ │ │ │ ├── poetry_core-1.5.0-py3-none-any.whl │ │ │ │ ├── poetry_core-2.0.1-py3-none-any.whl │ │ │ │ ├── setuptools-67.6.1-py3-none-any.whl │ │ │ │ └── wheel-0.40.0-py3-none-any.whl │ │ │ ├── generate.py │ │ │ ├── json/ │ │ │ │ ├── attrs/ │ │ │ │ │ └── 17.4.0.json │ │ │ │ ├── attrs.json │ │ │ │ ├── black/ │ │ │ │ │ ├── 19.10b0.json │ │ │ │ │ └── 21.11b0.json │ │ │ │ ├── black.json │ │ │ │ ├── cleo/ │ │ │ │ │ └── 1.0.0a5.json │ │ │ │ ├── cleo.json │ │ │ │ ├── clikit/ │ │ │ │ │ └── 0.2.4.json │ │ │ │ ├── clikit.json │ │ │ │ ├── colorama/ │ │ │ │ │ └── 0.3.9.json │ │ │ │ ├── colorama.json │ │ │ │ ├── discord-py/ │ │ │ │ │ └── 2.0.0.json │ │ │ │ ├── discord-py.json │ │ │ │ ├── filecache/ │ │ │ │ │ └── 0.81.json │ │ │ │ ├── filecache.json │ │ │ │ ├── funcsigs/ │ │ │ │ │ └── 1.0.2.json │ │ │ │ ├── funcsigs.json │ │ │ │ ├── futures/ │ │ │ │ │ └── 3.2.0.json │ │ │ │ ├── futures.json │ │ │ │ ├── hbmqtt/ │ │ │ │ │ └── 0.9.6.json │ │ │ │ ├── hbmqtt.json │ │ │ │ ├── importlib-metadata/ │ │ │ │ │ └── 1.7.0.json │ │ │ │ ├── importlib-metadata.json │ │ │ │ ├── ipython/ │ │ │ │ │ ├── 4.1.0rc1.json │ │ │ │ │ ├── 5.7.0.json │ │ │ │ │ └── 7.5.0.json │ │ │ │ ├── ipython.json │ │ │ │ ├── isodate/ │ │ │ │ │ └── 0.7.0.json │ │ │ │ ├── isodate.json │ │ │ │ ├── isort/ │ │ │ │ │ └── 4.3.4.json │ │ │ │ ├── isort-metadata.json │ │ │ │ ├── isort.json │ │ │ │ ├── jupyter/ │ │ │ │ │ └── 1.0.0.json │ │ │ │ ├── jupyter.json │ │ │ │ ├── mocked/ │ │ │ │ │ ├── invalid-version-package.json │ │ │ │ │ ├── isort-metadata/ │ │ │ │ │ │ └── 4.3.4.json │ │ │ │ │ ├── six-unknown-version/ │ │ │ │ │ │ └── 1.11.0.json │ │ │ │ │ ├── six-unknown-version.json │ │ │ │ │ ├── with-extra-dependency/ │ │ │ │ │ │ └── 0.12.4.json │ │ │ │ │ ├── with-extra-dependency.json │ │ │ │ │ ├── with-transitive-extra-dependency/ │ │ │ │ │ │ └── 0.12.4.json │ │ │ │ │ └── with-transitive-extra-dependency.json │ │ │ │ ├── more-itertools/ │ │ │ │ │ └── 4.1.0.json │ │ │ │ ├── more-itertools.json │ │ │ │ ├── pastel/ │ │ │ │ │ └── 0.1.0.json │ │ │ │ ├── pastel.json │ │ │ │ ├── pluggy/ │ │ │ │ │ └── 0.6.0.json │ │ │ │ ├── pluggy.json │ │ │ │ ├── poetry-core/ │ │ │ │ │ ├── 1.5.0.json │ │ │ │ │ └── 2.0.1.json │ │ │ │ ├── poetry-core.json │ │ │ │ ├── py/ │ │ │ │ │ └── 1.5.3.json │ │ │ │ ├── py.json │ │ │ │ ├── pylev/ │ │ │ │ │ └── 1.3.0.json │ │ │ │ ├── pylev.json │ │ │ │ ├── pytest/ │ │ │ │ │ ├── 3.5.0.json │ │ │ │ │ └── 3.5.1.json │ │ │ │ ├── pytest.json │ │ │ │ ├── python-language-server/ │ │ │ │ │ └── 0.21.2.json │ │ │ │ ├── python-language-server.json │ │ │ │ ├── pyyaml/ │ │ │ │ │ └── 3.13.0.json │ │ │ │ ├── pyyaml.json │ │ │ │ ├── requests/ │ │ │ │ │ ├── 2.18.0.json │ │ │ │ │ ├── 2.18.1.json │ │ │ │ │ ├── 2.18.2.json │ │ │ │ │ ├── 2.18.3.json │ │ │ │ │ ├── 2.18.4.json │ │ │ │ │ └── 2.19.0.json │ │ │ │ ├── requests.json │ │ │ │ ├── setuptools/ │ │ │ │ │ ├── 39.2.0.json │ │ │ │ │ └── 67.6.1.json │ │ │ │ ├── setuptools.json │ │ │ │ ├── six/ │ │ │ │ │ └── 1.11.0.json │ │ │ │ ├── six.json │ │ │ │ ├── sqlalchemy/ │ │ │ │ │ └── 1.2.12.json │ │ │ │ ├── sqlalchemy.json │ │ │ │ ├── toga/ │ │ │ │ │ ├── 0.3.0.json │ │ │ │ │ ├── 0.3.0dev1.json │ │ │ │ │ ├── 0.3.0dev2.json │ │ │ │ │ └── 0.4.0.json │ │ │ │ ├── toga.json │ │ │ │ ├── tomlkit/ │ │ │ │ │ ├── 0.5.2.json │ │ │ │ │ └── 0.5.3.json │ │ │ │ ├── tomlkit.json │ │ │ │ ├── twisted/ │ │ │ │ │ └── 18.9.0.json │ │ │ │ ├── twisted.json │ │ │ │ ├── wheel/ │ │ │ │ │ └── 0.40.0.json │ │ │ │ ├── wheel.json │ │ │ │ ├── zipp/ │ │ │ │ │ └── 3.5.0.json │ │ │ │ └── zipp.json │ │ │ ├── metadata/ │ │ │ │ ├── PyYAML-3.13-cp27-cp27m-win32.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp27-cp27m-win_amd64.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp34-cp34m-win32.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp34-cp34m-win_amd64.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp35-cp35m-win32.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp35-cp35m-win_amd64.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp36-cp36m-win32.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp36-cp36m-win_amd64.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp37-cp37m-win32.whl.metadata │ │ │ │ ├── PyYAML-3.13-cp37-cp37m-win_amd64.whl.metadata │ │ │ │ ├── attrs-17.4.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── black-19.10b0-py36-none-any.whl.metadata │ │ │ │ ├── black-21.11b0-py3-none-any.whl.metadata │ │ │ │ ├── cleo-1.0.0a5-py3-none-any.whl.metadata │ │ │ │ ├── clikit-0.2.4-py2.py3-none-any.whl.metadata │ │ │ │ ├── colorama-0.3.9-py2.py3-none-any.whl.metadata │ │ │ │ ├── discord.py-2.0.0-py3-none-any.whl.metadata │ │ │ │ ├── filecache-0.81-py3-none-any.whl.metadata │ │ │ │ ├── funcsigs-1.0.2-py2.py3-none-any.whl.metadata │ │ │ │ ├── futures-3.2.0-py2-none-any.whl.metadata │ │ │ │ ├── importlib_metadata-1.7.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── ipython-4.1.0rc1-py2.py3-none-any.whl.metadata │ │ │ │ ├── ipython-5.7.0-py2-none-any.whl.metadata │ │ │ │ ├── ipython-5.7.0-py3-none-any.whl.metadata │ │ │ │ ├── ipython-7.5.0-py3-none-any.whl.metadata │ │ │ │ ├── isodate-0.7.0-py3-none-any.whl.metadata │ │ │ │ ├── isort-4.3.4-py2-none-any.whl.metadata │ │ │ │ ├── isort-4.3.4-py3-none-any.whl.metadata │ │ │ │ ├── isort-metadata-4.3.4-py2-none-any.whl.metadata │ │ │ │ ├── isort-metadata-4.3.4-py3-none-any.whl.metadata │ │ │ │ ├── jupyter-1.0.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── mocked/ │ │ │ │ │ ├── with_extra_dependency-0.12.4-py3-none-any.whl.metadata │ │ │ │ │ └── with_transitive_extra_dependency-0.12.4-py3-none-any.whl.metadata │ │ │ │ ├── more_itertools-4.1.0-py2-none-any.whl.metadata │ │ │ │ ├── more_itertools-4.1.0-py3-none-any.whl.metadata │ │ │ │ ├── pastel-0.1.0-py3-none-any.whl.metadata │ │ │ │ ├── pluggy-0.6.0-py2-none-any.whl.metadata │ │ │ │ ├── pluggy-0.6.0-py3-none-any.whl.metadata │ │ │ │ ├── poetry_core-1.5.0-py3-none-any.whl.metadata │ │ │ │ ├── poetry_core-2.0.1-py3-none-any.whl.metadata │ │ │ │ ├── py-1.5.3-py2.py3-none-any.whl.metadata │ │ │ │ ├── pylev-1.3.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── pytest-3.5.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── pytest-3.5.1-py2.py3-none-any.whl.metadata │ │ │ │ ├── requests-2.18.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── requests-2.18.1-py2.py3-none-any.whl.metadata │ │ │ │ ├── requests-2.18.2-py2.py3-none-any.whl.metadata │ │ │ │ ├── requests-2.18.3-py2.py3-none-any.whl.metadata │ │ │ │ ├── requests-2.18.4-py2.py3-none-any.whl.metadata │ │ │ │ ├── requests-2.19.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── setuptools-39.2.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── setuptools-67.6.1-py3-none-any.whl.metadata │ │ │ │ ├── six-1.11.0-py2.py3-none-any.whl.metadata │ │ │ │ ├── toga-0.3.0-py3-none-any.whl.metadata │ │ │ │ ├── toga-0.3.0.dev1-py3-none-any.whl.metadata │ │ │ │ ├── toga-0.3.0.dev2-py3-none-any.whl.metadata │ │ │ │ ├── toga-0.4.0-py3-none-any.whl.metadata │ │ │ │ ├── tomlkit-0.5.2-py2.py3-none-any.whl.metadata │ │ │ │ ├── tomlkit-0.5.3-py2.py3-none-any.whl.metadata │ │ │ │ ├── wheel-0.40.0-py3-none-any.whl.metadata │ │ │ │ └── zipp-3.5.0-py3-none-any.whl.metadata │ │ │ ├── search/ │ │ │ │ ├── search-disallowed.html │ │ │ │ └── search.html │ │ │ └── stubbed/ │ │ │ ├── Twisted-18.9.0.tar.bz2 │ │ │ ├── attrs-17.4.0-py2.py3-none-any.whl │ │ │ ├── black-19.10b0-py36-none-any.whl │ │ │ ├── black-21.11b0-py3-none-any.whl │ │ │ ├── cleo-1.0.0a5-py3-none-any.whl │ │ │ ├── clikit-0.2.4-py2.py3-none-any.whl │ │ │ ├── colorama-0.3.9-py2.py3-none-any.whl │ │ │ ├── discord.py-2.0.0-py3-none-any.whl │ │ │ ├── futures-3.2.0-py2-none-any.whl │ │ │ ├── ipython-5.7.0-py2-none-any.whl │ │ │ ├── ipython-5.7.0-py3-none-any.whl │ │ │ ├── ipython-7.5.0-py3-none-any.whl │ │ │ ├── isodate-0.7.0-py3-none-any.whl │ │ │ ├── isort-4.3.4-py2-none-any.whl │ │ │ ├── isort-4.3.4-py3-none-any.whl │ │ │ ├── jupyter-1.0.0-py2.py3-none-any.whl │ │ │ ├── more_itertools-4.1.0-py2-none-any.whl │ │ │ ├── more_itertools-4.1.0-py3-none-any.whl │ │ │ ├── pastel-0.1.0-py3-none-any.whl │ │ │ ├── pluggy-0.6.0-py2-none-any.whl │ │ │ ├── pluggy-0.6.0-py3-none-any.whl │ │ │ ├── py-1.5.3-py2.py3-none-any.whl │ │ │ ├── pytest-3.5.0-py2.py3-none-any.whl │ │ │ ├── pytest-3.5.1-py2.py3-none-any.whl │ │ │ ├── requests-2.18.4-py2.py3-none-any.whl │ │ │ ├── six-1.11.0-py2.py3-none-any.whl │ │ │ ├── tomlkit-0.5.2-py2.py3-none-any.whl │ │ │ ├── tomlkit-0.5.3-py2.py3-none-any.whl │ │ │ └── zipp-3.5.0-py3-none-any.whl │ │ ├── pypi.py │ │ ├── python_hosted.py │ │ └── single-page/ │ │ ├── jax_releases.html │ │ └── mmcv_torch_releases.html │ ├── link_sources/ │ │ ├── __init__.py │ │ ├── test_base.py │ │ ├── test_html.py │ │ └── test_json.py │ ├── parsers/ │ │ ├── __init__.py │ │ ├── test_html_page_parser.py │ │ └── test_pypi_search_parser.py │ ├── test_cached_repository.py │ ├── test_http_repository.py │ ├── test_installed_repository.py │ ├── test_legacy_repository.py │ ├── test_lockfile_repository.py │ ├── test_pypi_repository.py │ ├── test_repository.py │ ├── test_repository_pool.py │ └── test_single_page_repository.py ├── test_conftest.py ├── test_factory.py ├── test_helpers.py ├── types.py ├── utils/ │ ├── __init__.py │ ├── conftest.py │ ├── env/ │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── python/ │ │ │ ├── __init__.py │ │ │ ├── test_manager.py │ │ │ ├── test_python_installer.py │ │ │ └── test_python_providers.py │ │ ├── test_env.py │ │ ├── test_env_manager.py │ │ ├── test_env_site_packages.py │ │ └── test_system_env.py │ ├── fixtures/ │ │ └── pyproject.toml │ ├── test_authenticator.py │ ├── test_cache.py │ ├── test_dependency_specification.py │ ├── test_extras.py │ ├── test_helpers.py │ ├── test_isolated_build.py │ ├── test_log_utils.py │ ├── test_password_manager.py │ ├── test_patterns.py │ ├── test_pip.py │ ├── test_python_manager.py │ └── test_threading.py └── vcs/ └── git/ ├── conftest.py ├── git_fixture.py ├── test_backend.py └── test_system.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cirrus.yml ================================================ tests_task: # We only use Cirrus CI for FreeBSD at present; the rest of the task will assume FreeBSD. freebsd_instance: image_family: freebsd-14-3 # Cirrus has a concurrency limit of 8 vCPUs for FreeBSD. Allow executing 4 tasks in parallel. cpu: 2 memory: 2G env: matrix: - PYTHON: python3.11 PYTHON_VERSION: 3.11 PYTHON_PACKAGE: python311 SQLITE_PACKAGE: py311-sqlite3 # FIXME: Python 3.12 is not available in Ports. # - PYTHON: python3.12 # PYTHON_VERSION: 3.12 # PYTHON_PACKAGE: python312 # SQLITE_PACKAGE: py312-sqlite3 # FIXME: use pipx for install. pipx is currently broken in Ports. POETRY_HOME: /opt/poetry # SHELL is not set by default, and we have tests that depend on it. SHELL: sh bootstrap_poetry_script: - pkg install -y git $PYTHON_PACKAGE $SQLITE_PACKAGE - $PYTHON -m venv $POETRY_HOME - $POETRY_HOME/bin/pip install poetry - echo "PATH=${POETRY_HOME}/bin:${PATH}" >> $CIRRUS_ENV setup_environment_script: # TODO: caching - poetry install - poetry env info - poetry show matrix: - alias: pytest name: "Tests / FreeBSD (Python ${PYTHON_VERSION}) / pytest" skip: "!changesInclude('.cirrus.yml', 'poetry.lock', 'pyproject.toml', 'src/**.py', 'tests/**')" pytest_script: poetry run pytest --integration -v --junitxml=junit.xml on_failure: annotate_failure_artifacts: path: junit.xml format: junit type: text/xml status_task: name: "Tests / FreeBSD Status" depends_on: - pytest container: image: alpine:latest cpu: 0.5 memory: 512M # No-op the clone. clone_script: true ================================================ FILE: .gitattributes ================================================ # Do not mess with line endings in metadata files or the hash will be wrong. *.metadata binary ================================================ FILE: .github/ISSUE_TEMPLATE/---bug-report.yml ================================================ name: "\U0001F41E Bug Report" labels: ["kind/bug", "status/triage"] description: "Poetry not working the way it is documented?" body: - type: markdown attributes: value: | Thank you for taking the time to file a complete bug report. Before submitting your issue, please review the [Before submitting a bug report](https://python-poetry.org/docs/contributing/#before-submitting-a-bug-report) section of our documentation. - type: textarea attributes: label: Description description: | Please describe what happened, with as much pertinent information as you can. Feel free to use markdown syntax. Also, ensure that the issue is not already fixed in the [latest](https://github.com/python-poetry/poetry/releases/latest) Poetry release. validations: required: true - type: textarea attributes: label: Workarounds description: | Is there a mitigation or workaround that allows users to avoid the issue today? validations: required: true - type: dropdown attributes: label: Poetry Installation Method description: | How did you install Poetry? options: - "pipx" - "install.python-poetry.org" - "system package manager (eg: dnf, apt etc.)" - "pip" - "other" validations: required: true - type: input attributes: label: Operating System description: | What Operating System are you using? placeholder: "Fedora 39" validations: required: true - type: input attributes: label: Poetry Version description: | Please attach output from `poetry --version` validations: required: true - type: textarea attributes: label: Poetry Configuration description: | Please attach output from `poetry config --list` render: 'bash session' validations: required: true - type: textarea attributes: label: Python Sysconfig description: | Please attach output from `python -m sysconfig` Note:_ You can paste the output into the placeholder below. If it is too long, you can attach it as a file. value: |
sysconfig.log ``` Paste the output of 'python -m sysconfig', over this line. ```
validations: required: false - type: textarea attributes: label: Example pyproject.toml description: | Please provide an example `pyproject.toml` demonstrating the issue. render: 'TOML' validations: required: false - type: textarea attributes: label: Poetry Runtime Logs description: | Please attach logs from the failing command using `poetry -vvv ` Note:_ You can paste the output into the placeholder below. If it is too long, you can attach it as a file. value: |
poetry-runtime.log ``` Paste the output of 'poetry -vvv ', over this line. ```
validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/---documentation.yml ================================================ name: "\U0001F4DA Documentation Issue" labels: ["area/docs", "status/triage"] description: "Did you find errors, omissions, or anything unintelligible in the documentation?" body: - type: markdown attributes: value: | Thank you for taking the time to file a complete bug report. Before submitting your issue, please review the [Suggesting enhancements](https://python-poetry.org/docs/contributing/#suggesting-enhancements) section of our documentation. Please also confirm the following: - You have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repository and believe that this is not a duplicate. - You have searched the [FAQ](https://python-poetry.org/docs/faq/) and general [documentation](https://python-poetry.org/docs/) and believe that your question is not already covered. - type: dropdown attributes: label: Issue Kind description: | What best describes the issue? options: - "Improving documentation" - "Missing documentation" - "Error in existing documentation" - "Unclear documentation" - "Other concerns with documentation" validations: required: true - type: input attributes: label: Existing Link description: | If the documentation in question exists, please provide a link to it. placeholder: "https://python-poetry.org/docs/dependency-specification/#version-constraints" validations: required: true - type: textarea attributes: label: Description description: | Please describe the feature, with as much pertinent information as you can. Feel free to use markdown syntax. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/---feature-request.yml ================================================ name: "\U0001F381 Feature Request" labels: ["kind/feature", "status/triage"] description: "Want something new?" body: - type: markdown attributes: value: | Thank you for taking the time to file a complete bug report. Before submitting your issue, please search [issues](https://github.com/python-poetry/poetry/issues) to ensure this is not a duplicate. If the issue is trivial, why not submit a pull request instead? - type: dropdown attributes: label: Issue Kind description: | What best describes this issue? options: - "Brand new capability" - "Change in current behaviour" - "Other" validations: required: true - type: textarea attributes: label: Description description: | Please describe the issue, with as much pertinent information as you can. Feel free to use markdown syntax. Also, ensure that the issue is not already fixed in the [development documentation](https://python-poetry.org/docs/main/). validations: required: true - type: textarea attributes: label: Impact description: | Please describe the motivation for this issue. Describe, as best you can, how this improves or impacts the users of Poetry and why this is important. validations: required: true - type: textarea attributes: label: Workarounds description: | Is there a mitigation, workaround, or addon that allows users to achieve the same functionality today? validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: false contact_links: - name: '💬 Discussions' url: https://github.com/python-poetry/poetry/discussions about: | Ask questions about using Poetry, Poetry's features and roadmap, or get support and feedback for your usage of Poetry. - name: '💬 Discord Server' url: https://discordapp.com/invite/awxPgve about: | Chat with the community and Poetry maintainers about both the usage of and development of the project. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # Pull Request Check List Resolves: #issue-number-here - [ ] Added **tests** for changed code. - [ ] Updated **documentation** for changed code. ================================================ FILE: .github/actions/bootstrap-poetry/action.yaml ================================================ name: Bootstrap Poetry description: Configure the environment with the specified Python and Poetry version. inputs: python-version: description: Desired node-semver compatible Python version expression (or 'default') default: 'default' python-latest: description: Use an uncached Python if a newer match is available default: 'false' poetry-spec: description: pip-compatible installation specification to use for Poetry default: 'poetry' outputs: python-path: description: Path to the installed Python interpreter value: ${{ steps.setup-python.outputs.python-path }} python-version: description: Version of the installed Python interpreter value: ${{ steps.setup-python.outputs.python-version }} runs: using: composite steps: - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 id: setup-python if: inputs.python-version != 'default' with: python-version: ${{ inputs.python-version }} check-latest: ${{ inputs.python-latest == 'true' }} allow-prereleases: true update-environment: false - run: pipx install ${PYTHON_PATH:+--python "$PYTHON_PATH"} "${POETRY_SPEC}" shell: bash env: PYTHON_PATH: ${{ inputs.python-version != 'default' && steps.setup-python.outputs.python-path || '' }} POETRY_SPEC: ${{ inputs.poetry-spec }} # Enable handling long path names (+260 char) on the Windows platform # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation - run: git config --system core.longpaths true if: runner.os == 'Windows' shell: pwsh # Use Poetry Python for virtual environments # (Otherwise, the system Python will be used per default instead of the Python version we just installed) - run: poetry config virtualenvs.use-poetry-python true shell: bash ================================================ FILE: .github/actions/poetry-install/action.yaml ================================================ name: Poetry Install description: Run `poetry install` with optional artifact and metadata caching inputs: args: description: Arguments for `poetry install` cache: description: Enable transparent Poetry artifact and metadata caching default: 'true' outputs: cache-hit: description: Whether an exact cache hit occured value: ${{ steps.cache.outputs.cache-hit }} runs: using: composite steps: - run: printf 'cache-dir=%s\n' "$(poetry config cache-dir)" >> $GITHUB_OUTPUT id: poetry-config shell: bash # Bust the cache every 24 hours to prevent it from expanding over time. - run: printf 'date=%s\n' "$(date -I)" >> $GITHUB_OUTPUT id: get-date if: inputs.cache == 'true' shell: bash - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 id: cache if: inputs.cache == 'true' with: path: | ${{ steps.poetry-config.outputs.cache-dir }}/artifacts ${{ steps.poetry-config.outputs.cache-dir }}/cache key: poetry-${{ steps.get-date.outputs.date }}-${{ runner.os }}-${{ hashFiles('pyproject.toml', 'poetry.lock') }} # The cache is cross-platform, and other platforms are used to seed cache misses. restore-keys: | poetry-${{ steps.get-date.outputs.date }}-${{ runner.os }}- poetry-${{ steps.get-date.outputs.date }}- enableCrossOsArchive: true - run: poetry install ${ARGS} shell: bash env: ARGS: ${{ inputs.args }} - run: poetry env info shell: bash - run: poetry show shell: bash ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" cooldown: default-days: 7 labels: - "area/ci" # Grouped updates to reduce PR noise. # # See: # - https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/optimizing-pr-creation-version-updates#prioritizing-meaningful-updates # - https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#groups-- - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" cooldown: default-days: 7 labels: - "area/project/deps" groups: production-dependencies: dependency-type: "production" development-dependencies: dependency-type: "development" open-pull-requests-limit: 2 ================================================ FILE: .github/scripts/backport.sh ================================================ #!/usr/bin/env bash # # Copyright 2024 Bjorn Neergaard # # This work is licensed under the terms of the MIT license. # For a copy, see . # # This script is used to automatically create backport pull requests. It is # smart enough to require no arguments if run against a PR branch, assuming the # upstream repo conforms to the default prefixes. Target branches will be # determined by labels present on the original pull request. # # It is capable of handling PRs merged with a commit, by rebase and by # squash-and-rebase. The backport pull request will have its title, body and # labels derived from the original. The cherry-picked comments can be signed off, # and a comment will be created if requested. # # In particular, this script assumes the 'origin' remote points to the target # repository for backports. We also assume we can freely clobber local and remote # branches using our backport branch naming scheme and that you don't mind if we # prune your worktrees for you. # # This script can push the backport branches to a fork if a corresponding # --remote is passed. basename=${0##*/} # Check for Homebrew-installed getopt on macOS. for g in /{usr/local,opt/homebrew}/opt/gnu-getopt/bin/getopt; do test -x "$g" && : GETOPT="${GETOPT:=$g}" && break done || : GETOPT="${GETOPT:=getopt}" "${GETOPT}" --test >/dev/null if [ $? -ne 4 ]; then printf >&2 '%s: GNU getopt is required, please ensure it is available in the PATH\n' "$basename" exit 1 fi if ! command -v gh >/dev/null; then printf >&2 "%s: the GitHub CLI (\`gh') is required, please ensure it is available in the PATH\n" "$basename" fi usage() { printf >&2 '%s -h|--help\n' "$basename" printf >&2 '%s [-s|--signoff] [-c|--comment] --pr [pr] --remote [remote] --branch-prefix [prefix] --label-prefix [prefix]\n' "$basename" } args="$("${GETOPT}" -o h,s,c -l help,signoff,comment,pr:,remote:,branch-prefix:,label-prefix: -n "$basename" -- "$@")" || exit $? eval set -- "$args" unset args while [ "$#" -gt 0 ]; do case "$1" in --pr | --remote | --branch-prefix | --label-prefix) flag="${1:2}" printf -v "${flag//-/_}" '%s' "$2" shift 2 ;; -s | --signoff) signoff=1 shift ;; -c | --comment) comment=1 shift ;; -h | --help) usage exit ;; -- | *) shift break ;; esac done set -eux -o pipefail # Determine the number of the target pull request, if not already supplied. : pr="${pr:=$(gh pr view --json number --jq '.number')}" # Use the 'origin' remote by default; if a fork is desired, a corresponding remote should # be specified, e.g. `gh repo fork --remote-name fork` and `--remote fork`. : remote="${remote:=origin}" # Use 'backport/' as a default prefix for both the resulting branch and the triggering label. : branch_prefix="${branch_prefix:=backport/}" : label_prefix="${label_prefix:=backport/}" # Determine the owner of the target remote (necessary to open a pull request) based on the URL. remote_owner=$(basename "$(dirname "$(git remote get-url --push "$remote")")") # Get the state, base branch and merge commit (if it exists) of the pull request. pr_meta=$(gh pr view --json state,baseRefName,mergeCommit --jq '[.state,.baseRefName,.mergeCommit.oid][]' "$pr") pr_state=$(sed '1q;d' <<<"$pr_meta") pr_base=$(sed '2q;d' <<<"$pr_meta") pr_mergecommit=$(sed '3q;d' <<<"$pr_meta") # Get the list of commits present in the pull request. pr_commits=$(gh pr view --json commits --jq '.commits[].oid' "$pr") # Get the title and body of the pull request. pr_title_body=$(gh pr view --json title,body --jq '[.title,.body][]' "$pr") pr_title=$(head -n 1 <<<"$pr_title_body") pr_body=$(tail -n +2 <<<"$pr_title_body") # Gather the list of labels on the pull request. pr_labels=$(gh pr view --json labels --jq '.labels[].name' "$pr") # Fetch origin, to ensure we have the latest commits on all upstream branches. git fetch origin # Fetch the latest pull request head, to ensure we have all commits available locally. # It will be available as FETCH_HEAD for the remainder of this script. git fetch origin "refs/pull/${pr}/head" # Determine which commits should be cherry-picked. This can be surprisingly complex, # but the typical cases present on GitHub are handled here. if [ "$pr_state" = OPEN ] || [ "$(git rev-list --no-walk --count --merges "$pr_mergecommit")" -eq 1 ]; then # Unmerged, or merge commit: the list of commits is equivalent to the pull request. backport_commits=$pr_commits else # The cherry commits represent those commits that were cherry-picked from the pull request to the base. pr_cherry_commits=$(git cherry refs/remotes/origin/main FETCH_HEAD | sed -n '/^- / s/- //p') # The rebased commits represent those commits present in the base that correspond to the pull request. pr_rebased_commits=$(git cherry FETCH_HEAD refs/remotes/origin/main | sed -n '/^- / s/- //p') # Look for cherry-picks (which is what a conflict-free and non-interactive rebase merge # effectively does). Note that a squash confuses the list of rebased commits; # to make our heuristics as effective as possible, we have two checks: # * Git must successfully identify the list of commits cherry-picked from the PR. # * The number of commits in the pull request and identified for backport must match. if [ "$pr_cherry_commits" = "$pr_commits" ] \ && [ "$(wc -l <<<"$pr_rebased_commits")" -eq "$(wc -l <<<"$pr_commits")" ]; then # Rebase: the list of commits is those rebased into the base branch. backport_commits=$pr_rebased_commits else # Squash-and-rebase: the list of commits is the singular merged commit. backport_commits=$pr_mergecommit fi fi # Create a temporary directory in which to hold worktrees for each backport attempt. workdir="$(mktemp -d)" trap 'rm -rf "${workdir}"; git worktree prune -v' EXIT # Create some arrays to track success and failure. backport_urls=() failed_backports=() # Iterate over all labels matching the prefix to determine what branches must be backported. while IFS= read -r backport_label; do target_branch="${backport_label/#${label_prefix}}" backport_branch="${branch_prefix}${pr}-${target_branch}" # Check that the target branch and base branch are not the same. This heads off some # potential errors. if [ "$target_branch" = "$pr_base" ]; then continue fi # Create a new backport branch, in a new worktree, based on the target branch. backport_worktree="${workdir}/${backport_branch}" git worktree add -B "$backport_branch" "$backport_worktree" "refs/remotes/origin/${target_branch}" # Cherry-pick the commits from the target branch in order. for commit in $backport_commits; do if ! git -C "$backport_worktree" cherry-pick -x ${signoff:+-s} "$commit"; then # If a cherry-pick fails, record the branch and move on. failed_backports+=("$target_branch") continue 2 fi done # Push the resulting backport branch to the configured remote. git push -f "$remote" "$backport_branch" # Create a derived title and label for the PR. backport_title="[${target_branch} backport] ${pr_title}" backport_body="Backport #${pr} to ${target_branch}." if [ -n "$pr_body" ]; then backport_body+=$'\n\n'"---"$'\n\n'"$pr_body" fi # Determine which labels should be brought over to the new pull request, formatted as the CLI expects. backport_labels=$(grep -v "^${label_prefix}" <<<"$pr_labels" | head -c -1 | tr '\n' ',') # Check for any open backports; note this is a heuristic as we just grab the first pull request # that matches our generated branch name. This is unlikely to fail as we filter by author, however. backport_url=$(gh pr list --author '@me' --head "$backport_branch" --json url --jq 'first(.[].url)') if [ -n "$backport_url" ]; then # Update the pull request title and body. # TODO: update labels? gh pr edit "$backport_url" --title "$backport_title" --body "$backport_body" found_backport=1 else # Create a new pull request from the backport branch, against the target branch. backport_url=$(gh pr create --base "$target_branch" --head "${remote_owner}:${backport_branch}" \ --title "$backport_title" --body-file - \ --label "$backport_labels" \ <<<"$backport_body" | tail -n 1) fi # Track this successful backport. backport_urls+=("$backport_url") done < <(grep "^${label_prefix}" <<<"$pr_labels") if [ -n "${comment:-}" ]; then # Generate a comment on the original PR, recording what backports we opened (or failed to open). if [ "${#backport_urls[@]}" -gt 0 ]; then comment_body+="Automated backport PRs opened:"$'\n' for backport_url in "${backport_urls[@]}"; do comment_body+="* ${backport_url}"$'\n' done comment_body+=$'\n' fi if [ "${#failed_backports[@]}" -gt 0 ]; then comment_body+="Backports failed on the following branches:" for failed_backport in "${failed_backports[@]}"; do comment_body+="* ${failed_backport}"$'\n' done comment_body+=$'\n' # If we're running in GitHub actions, link to the run log to diagnose why a backport failed. if [ -n "${GITHUB_ACTIONS:-}" ]; then comment_body+="Inspect the run at ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" fi fi if [ -n "$comment_body" ]; then # If we had any matches for an existing PR, we'll go ahead and assume we already commented. # Edit the existing comment instead. gh pr comment "$pr" ${found_backport:+--edit-last} --body-file - <<<"$comment_body" fi fi ================================================ FILE: .github/workflows/.tests-matrix.yaml ================================================ # Reusable workflow consumed by tests.yaml; used to share a single matrix across jobs. on: workflow_call: inputs: runner: required: true type: string python-version: required: true type: string run-mypy: required: true type: boolean run-pytest: required: true type: boolean run-pytest-export: required: true type: boolean defaults: run: shell: bash env: PYTHONWARNDEFAULTENCODING: 'true' jobs: mypy: name: mypy runs-on: ${{ inputs.runner }} if: inputs.run-mypy steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: ./.github/actions/bootstrap-poetry id: bootstrap-poetry with: python-version: ${{ inputs.python-version }} - uses: ./.github/actions/poetry-install - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: .mypy_cache key: mypy-${{ runner.os }}-py${{ steps.bootstrap-poetry.outputs.python-version }}-${{ hashFiles('pyproject.toml', 'poetry.lock') }} restore-keys: | mypy-${{ runner.os }}-py${{ steps.bootstrap-poetry.outputs.python-version }}- mypy-${{ runner.os }}- - run: poetry run mypy pytest: name: pytest runs-on: ${{ inputs.runner }} if: inputs.run-pytest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: ./.github/actions/bootstrap-poetry with: python-version: ${{ inputs.python-version }} - uses: ./.github/actions/poetry-install with: args: --with github-actions - run: poetry run pytest --integration -v env: POETRY_TEST_INTEGRATION_GIT_USERNAME: ${{ github.actor }} POETRY_TEST_INTEGRATION_GIT_PASSWORD: ${{ github.token }} - run: git diff --exit-code --stat HEAD pytest-export: name: pytest (poetry-plugin-export) runs-on: ${{ inputs.runner }} if: inputs.run-pytest-export steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false path: poetry - uses: ./poetry/.github/actions/bootstrap-poetry with: python-version: ${{ inputs.python-version }} - name: Get poetry-plugin-export version run: | PLUGIN_VERSION=$(curl -s https://pypi.org/pypi/poetry-plugin-export/json | jq -r ".info.version") echo "Found version ${PLUGIN_VERSION}" echo version=${PLUGIN_VERSION} >> $GITHUB_OUTPUT id: poetry-plugin-export-version - name: Check out poetry-plugin-export uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false path: poetry-plugin-export repository: python-poetry/poetry-plugin-export # use main for now because of poetry-core#826 # ref: refs/tags/${{ steps.poetry-plugin-export-version.outputs.version }} - name: Use local poetry working-directory: poetry-plugin-export # Replace the python version to avoid conflicts # if the plugin still supports a wider range than Poetry itself. run: | perl -pi -e 's/^requires-python =.*$/requires-python = "~='"${PYTHON_VERSION}"'"/' pyproject.toml poetry remove --lock poetry-core # use whatever poetry uses poetry add --lock --group dev ../poetry env: PYTHON_VERSION: ${{ inputs.python-version }} # This step can be removed after having released a poetry-plugin-export version # that has cffi>=1.17.0 in its lock file. - name: Force more recent cffi (workaround for Python 3.13) working-directory: poetry-plugin-export run: poetry update --lock cffi - name: Install working-directory: poetry-plugin-export run: poetry install - name: Run tests working-directory: poetry-plugin-export run: poetry run pytest -v - name: Check for clean working tree working-directory: poetry-plugin-export run: | git checkout -- pyproject.toml poetry.lock git diff --exit-code --stat HEAD ================================================ FILE: .github/workflows/backport.yaml ================================================ name: Backport on: pull_request_target: # zizmor: ignore[dangerous-triggers] types: - closed - labeled # we create the token we need later on permissions: {} jobs: backport: name: Create backport runs-on: ubuntu-latest # This workflow only applies to merged PRs; and triggers on a PR being closed, or the backport label being applied. # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. if: > github.event.pull_request.merged && ( github.event.action == 'closed' || (github.event.action == 'labeled' && contains(github.event.label.name, 'backport/') ) ) steps: - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 id: app-token with: app-id: ${{ secrets.POETRY_TOKEN_APP_ID }} private-key: ${{ secrets.POETRY_TOKEN_APP_KEY }} - uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2.0.4 with: github_token: ${{ steps.app-token.outputs.token }} title_template: "[<%= base %>] <%= title %>" label_pattern: "^backport/(?([^ ]+))$" ================================================ FILE: .github/workflows/docs.yaml ================================================ name: Documentation Preview on: pull_request: # allow repository maintainers to modify and test workflow paths: - ".github/workflows/docs.yaml" pull_request_target: # zizmor: ignore[dangerous-triggers] # enable runs for this workflow when labeled as documentation only # prevent execution when the workflow itself is modified from a fork types: - labeled - synchronize paths: - "docs/**" jobs: deploy: name: Build & Deploy runs-on: ubuntu-latest if: > (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'impact/docs')) || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) permissions: contents: read pull-requests: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: python-poetry/website # use .github from pull request target instead of pull_request.head # for pull_request_target trigger to avoid arbitrary code execution - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false path: poetry-github sparse-checkout: .github # only checkout docs from pull_request.head to not use something else by accident # for pull_request_target trigger (security) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false path: poetry-docs ref: ${{ github.event.pull_request.head.sha }} sparse-checkout: docs - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: "18" - uses: ./poetry-github/.github/actions/bootstrap-poetry - uses: ./poetry-github/.github/actions/poetry-install with: args: --no-root --only main - name: website-build run: | # Rebuild the docs files from the PR checkout. poetry run python bin/website build --local ./poetry-docs # Build website assets (CSS/JS). npm ci && npm run prod # Build the static website. npx hugo --minify --logLevel info - uses: amondnet/vercel-action@888da851026e0573da056b061931bcb765a915c4 # v41.1.4 with: vercel-version: 41.1.4 vercel-token: ${{ secrets.VERCEL_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} scope: python-poetry github-comment: true working-directory: public ================================================ FILE: .github/workflows/lock-threads.yaml ================================================ name: Lock Threads on: schedule: - cron: '0 0 * * *' # every day at midnight workflow_dispatch: concurrency: group: ${{ github.workflow }} jobs: lock-issues: runs-on: ubuntu-latest permissions: issues: write steps: - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 with: process-only: issues issue-inactive-days: 30 issue-comment: > This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. lock-prs: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 with: process-only: prs pr-inactive-days: 30 pr-comment: > This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: release: types: [published] permissions: {} jobs: build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - run: pipx run build - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: distfiles path: dist/ if-no-files-found: error upload-github: name: Upload (GitHub) runs-on: ubuntu-latest permissions: contents: write needs: build steps: # We need to be in a git repo for gh to work. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: distfiles path: dist/ - run: gh release upload "${TAG_NAME}" dist/*.{tar.gz,whl} env: GH_TOKEN: ${{ github.token }} TAG_NAME: ${{ github.event.release.tag_name }} upload-pypi: name: Upload (PyPI) runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/project/poetry/ permissions: id-token: write needs: build steps: - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: distfiles path: dist/ - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: print-hash: true ================================================ FILE: .github/workflows/tests.yaml ================================================ name: Tests on: push: pull_request: merge_group: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} defaults: run: shell: bash permissions: {} jobs: changes: name: Detect changed files runs-on: ubuntu-latest outputs: project: ${{ steps.changes.outputs.project }} fixtures-pypi: ${{ steps.changes.outputs.fixtures-pypi }} src: ${{ steps.changes.outputs.src }} tests: ${{ steps.changes.outputs.tests }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: changes with: filters: | workflow: &workflow - '.github/actions/**' - '.github/workflows/tests.yaml' - '.github/workflows/.tests-matrix.yaml' project: &project - *workflow - 'poetry.lock' - 'pyproject.toml' fixtures-pypi: - *workflow - 'tests/repositories/fixtures/pypi.org/**' src: - *project - 'src/**/*.py' tests: - *project - 'src/**/*.py' - 'tests/**' lockfile: name: Check poetry.lock runs-on: ubuntu-latest if: needs.changes.outputs.project == 'true' needs: changes steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: ./.github/actions/bootstrap-poetry - run: poetry check --lock smoke: name: Smoke-test build and install runs-on: ubuntu-latest if: needs.changes.outputs.project == 'true' needs: lockfile steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - run: pipx run build - run: pipx run twine check --strict dist/* - run: pipx install --suffix=@build dist/*.whl - uses: ./.github/actions/bootstrap-poetry # Smoke test: confirm the version of the installed wheel matches the project. - run: poetry@build --version | grep $(poetry version --short) fixtures-pypi: name: Check fixtures (PyPI) runs-on: ubuntu-latest if: needs.changes.outputs.fixtures-pypi == 'true' needs: changes steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: ./.github/actions/bootstrap-poetry - uses: ./.github/actions/poetry-install with: args: --only main,test - run: poetry run env PYTHONPATH="$GITHUB_WORKSPACE" python tests/repositories/fixtures/pypi.org/generate.py - run: git diff --exit-code --stat HEAD tests/repositories/fixtures/pypi.org tests-matrix: # Use this matrix with multiple jobs defined in a reusable workflow: uses: ./.github/workflows/.tests-matrix.yaml name: "${{ matrix.os.name }} (Python ${{ matrix.python-version }})" if: '!failure()' needs: - lockfile - changes with: runner: ${{ matrix.os.image }} python-version: ${{ matrix.python-version }} run-mypy: ${{ needs.changes.outputs.tests == 'true' }} run-pytest: ${{ needs.changes.outputs.tests == 'true' }} run-pytest-export: ${{ needs.changes.outputs.src == 'true' }} secrets: inherit # zizmor: ignore[secrets-inherit] strategy: matrix: os: - name: Ubuntu image: ubuntu-22.04 - name: Windows image: windows-2022 - name: macOS aarch64 image: macos-14 python-version: - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" fail-fast: false status: name: Status runs-on: ubuntu-latest if: always() needs: - lockfile - smoke - fixtures-pypi - tests-matrix steps: - run: ${{ (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && 'false' || 'true' }} ================================================ FILE: .gitignore ================================================ *.pyc # Packages /dist/* # Unit test / coverage reports .coverage .pytest_cache .DS_Store .idea/* .python-version .vscode/* /docs/site/* .mypy_cache .venv /poetry.toml ================================================ FILE: .pre-commit-config.yaml ================================================ ci: autofix_prs: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace exclude: tests/repositories/fixtures/pypi.org/metadata/.*\.metadata - id: end-of-file-fixer exclude: ^.*\.egg-info/|tests/repositories/fixtures/pypi.org/metadata/.*\.metadata - id: check-merge-conflict exclude: tests/repositories/fixtures/installed/vendor/py3.7/attrs-19.3.0.dist-info/METADATA - id: check-case-conflict - id: check-json - id: check-toml exclude: tests/fixtures/invalid_lock/poetry\.lock - id: check-yaml - id: pretty-format-json args: [--autofix, --no-ensure-ascii, --no-sort-keys] - id: check-ast - id: debug-statements - id: check-docstring-first - repo: https://github.com/pre-commit/pre-commit rev: v4.5.1 hooks: - id: validate_manifest - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.4 hooks: - id: ruff-check - id: ruff-format - repo: https://github.com/woodruffw/zizmor-pre-commit rev: v1.22.0 hooks: - id: zizmor ================================================ FILE: .pre-commit-hooks.yaml ================================================ - id: poetry-check name: poetry-check description: run poetry check to validate config entry: poetry check language: python pass_filenames: false files: ^(.*/)?(poetry\.lock|pyproject\.toml)$ - id: poetry-lock name: poetry-lock description: run poetry lock to update lock file entry: poetry lock language: python pass_filenames: false files: ^(.*/)?(poetry\.lock|pyproject\.toml)$ - id: poetry-install name: poetry-install description: run poetry install to install dependencies from the lock file entry: poetry install language: python pass_filenames: false stages: [post-checkout, post-merge] always_run: true ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## [2.3.2] - 2026-02-01 ### Changed - Allow `dulwich>=1.0` ([#10701](https://github.com/python-poetry/poetry/pull/10701)). ### poetry-core ([`2.3.1`](https://github.com/python-poetry/poetry-core/releases/tag/2.3.1)) - Fix an issue where `platform_release` could not be parsed on Windows Server ([#911](https://github.com/python-poetry/poetry-core/pull/911)). ## [2.3.1] - 2026-01-20 ### Fixed - Fix an issue where cached information about each package was always considered outdated ([#10699](https://github.com/python-poetry/poetry/pull/10699)). ### Docs - Document SHELL_VERBOSITY environment variable ([#10678](https://github.com/python-poetry/poetry/pull/10678)). ## [2.3.0] - 2026-01-18 ### Added - **Add support for exporting `pylock.toml` files with `poetry-plugin-export`** ([#10677](https://github.com/python-poetry/poetry/pull/10677)). - Add support for specifying build constraints for dependencies ([#10388](https://github.com/python-poetry/poetry/pull/10388)). - Add support for publishing artifacts whose version is determined dynamically by the build-backend ([#10644](https://github.com/python-poetry/poetry/pull/10644)). - Add support for editable project plugins ([#10661](https://github.com/python-poetry/poetry/pull/10661)). - Check `requires-poetry` before any other validation ([#10593](https://github.com/python-poetry/poetry/pull/10593)). - Validate the content of `project.readme` when running `poetry check` ([#10604](https://github.com/python-poetry/poetry/pull/10604)). - Add the option to clear all caches by making the cache name in `poetry cache clear` optional ([#10627](https://github.com/python-poetry/poetry/pull/10627)). - Automatically update the cache for packages where the locked files differ from cached files ([#10657](https://github.com/python-poetry/poetry/pull/10657)). - Suggest to clear the cache if running a command with `--no-cache` solves an issue ([#10585](https://github.com/python-poetry/poetry/pull/10585)). - Propose `poetry init` when trying `poetry new` for an existing directory ([#10563](https://github.com/python-poetry/poetry/pull/10563)). - Add support for `poetry publish --skip-existing` for new Nexus OSS versions ([#10603](https://github.com/python-poetry/poetry/pull/10603)). - Show Poetry's own Python's path in `poetry debug info` ([#10588](https://github.com/python-poetry/poetry/pull/10588)). ### Changed - **Drop support for Python 3.9** ([#10634](https://github.com/python-poetry/poetry/pull/10634)). - **Change the default of `installer.re-resolve` from `true` to `false`** ([#10622](https://github.com/python-poetry/poetry/pull/10622)). - **PEP 735 dependency groups are considered in the lock file hash** ([#10621](https://github.com/python-poetry/poetry/pull/10621)). - Deprecate `poetry.utils._compat.metadata`, which is sometimes used in plugins, in favor of `importlib.metadata` ([#10634](https://github.com/python-poetry/poetry/pull/10634)). - Improve managing free-threaded Python versions with `poetry python` ([#10606](https://github.com/python-poetry/poetry/pull/10606)). - Prefer JSON API to HTML API in legacy repositories ([#10672](https://github.com/python-poetry/poetry/pull/10672)). - When running `poetry init`, only add the readme field in the `pyproject.toml` if the readme file exists ([#10679](https://github.com/python-poetry/poetry/pull/10679)). - Raise an error if no hash can be determined for any distribution link of a package ([#10673](https://github.com/python-poetry/poetry/pull/10673)). - Require `dulwich>=0.25.0` ([#10674](https://github.com/python-poetry/poetry/pull/10674)). ### Fixed - Fix an issue where `poetry remove` did not work for PEP 735 dependency groups with `include-group` items ([#10587](https://github.com/python-poetry/poetry/pull/10587)). - Fix an issue where `poetry remove` caused dangling `include-group` references in PEP 735 dependency groups ([#10590](https://github.com/python-poetry/poetry/pull/10590)). - Fix an issue where `poetry add` did not work for PEP 735 dependency groups with `include-group` items ([#10636](https://github.com/python-poetry/poetry/pull/10636)). - Fix an issue where PEP 735 dependency groups were not considered in the lock file hash ([#10621](https://github.com/python-poetry/poetry/pull/10621)). - Fix an issue where wrong markers were locked for a dependency that was required by several groups with different markers ([#10613](https://github.com/python-poetry/poetry/pull/10613)). - Fix an issue where non-deterministic markers were created in a method used by `poetry-plugin-export` ([#10667](https://github.com/python-poetry/poetry/pull/10667)). - Fix an issue where wrong wheels were chosen for installation in free-threaded Python environments if Poetry itself was not installed with free-threaded Python ([#10614](https://github.com/python-poetry/poetry/pull/10614)). - Fix an issue where `poetry publish` used the metadata of the project instead of the metadata of the build artifact ([#10624](https://github.com/python-poetry/poetry/pull/10624)). - Fix an issue where `poetry env use` just used another Python version instead of failing when the requested version was not supported by the project ([#10685](https://github.com/python-poetry/poetry/pull/10685)). - Fix an issue where `poetry env activate` returned the wrong command for `dash` ([#10696](https://github.com/python-poetry/poetry/pull/10696)). - Fix an issue where `data-dir` and `python.installation-dir` could not be set ([#10595](https://github.com/python-poetry/poetry/pull/10595)). - Fix an issue where Python and pip executables were not correctly detected on Windows ([#10645](https://github.com/python-poetry/poetry/pull/10645)). - Fix an issue where invalid template variables in `virtualenvs.prompt` caused an incomprehensible error message ([#10648](https://github.com/python-poetry/poetry/pull/10648)). ### Docs - Add a warning about `~/.netrc` for Poetry credential configuration ([#10630](https://github.com/python-poetry/poetry/pull/10630)). - Clarify that the local configuration takes precedence over the global configuration ([#10676](https://github.com/python-poetry/poetry/pull/10676)). - Add an explanation in which cases `packages` are automatically detected ([#10680](https://github.com/python-poetry/poetry/pull/10680)). ### poetry-core ([`2.3.0`](https://github.com/python-poetry/poetry-core/releases/tag/2.3.0)) - Normalize versions ([#893](https://github.com/python-poetry/poetry-core/pull/893)). - Fix an issue where unsatisfiable requirements did not raise an error ([#891](https://github.com/python-poetry/poetry-core/pull/891)). - Fix an issue where the implicit main group did not exist if it was explicitly declared as not having any dependencies ([#892](https://github.com/python-poetry/poetry-core/pull/892)). - Fix an issue where `python_full_version` markers with pre-release versions were parsed incorrectly ([#893](https://github.com/python-poetry/poetry-core/pull/893)). ## [2.2.1] - 2025-09-21 ### Fixed - Fix an issue where `poetry self show` failed with a message about an invalid output format ([#10560](https://github.com/python-poetry/poetry/pull/10560)). ### Docs - Remove outdated statements about dependency groups ([#10561](https://github.com/python-poetry/poetry/pull/10561)). ### poetry-core ([`2.2.1`](https://github.com/python-poetry/poetry-core/releases/tag/2.2.1)) - Fix an issue where it was not possible to declare a PEP 735 dependency group as optional ([#888](https://github.com/python-poetry/poetry-core/pull/888)). ## [2.2.0] - 2025-09-14 ### Added - **Add support for nesting dependency groups** ([#10166](https://github.com/python-poetry/poetry/pull/10166)). - **Add support for PEP 735 dependency groups** ([#10130](https://github.com/python-poetry/poetry/pull/10130)). - **Add support for PEP 639 license clarity** ([#10413](https://github.com/python-poetry/poetry/pull/10413)). - Add a `--format` option to `poetry show` to alternatively output json format ([#10487](https://github.com/python-poetry/poetry/pull/10487)). - Add official support for Python 3.14 ([#10514](https://github.com/python-poetry/poetry/pull/10514)). ### Changed - **Normalize dependency group names** ([#10387](https://github.com/python-poetry/poetry/pull/10387)). - Change `installer.no-binary` and `installer.only-binary` so that explicit package names will take precedence over `:all:` ([#10278](https://github.com/python-poetry/poetry/pull/10278)). - Improve log output during `poetry install` when a wheel is built from source ([#10404](https://github.com/python-poetry/poetry/pull/10404)). - Improve error message in case a file lock could not be acquired while cloning a git repository ([#10535](https://github.com/python-poetry/poetry/pull/10535)). - Require `dulwich>=0.24.0` ([#10492](https://github.com/python-poetry/poetry/pull/10492)). - Allow `virtualenv>=20.33` again ([#10506](https://github.com/python-poetry/poetry/pull/10506)). - Allow `findpython>=0.7` ([#10510](https://github.com/python-poetry/poetry/pull/10510)). - Allow `importlib-metadata>=8.7` ([#10511](https://github.com/python-poetry/poetry/pull/10511)). ### Fixed - Fix an issue where `poetry new` did not create the project structure in an existing empty directory ([#10431](https://github.com/python-poetry/poetry/pull/10431)). - Fix an issue where a dependency that was required for a specific Python version was not installed into an environment of a pre-release Python version ([#10516](https://github.com/python-poetry/poetry/pull/10516)). ### poetry-core ([`2.2.0`](https://github.com/python-poetry/poetry-core/releases/tag/2.2.0)) - Deprecate table values and values that are not valid SPDX expressions for `[project.license]` ([#870](https://github.com/python-poetry/poetry-core/pull/870)). - Fix an issue where explicitly included files that are in `.gitignore` were not included in the distribution ([#874](https://github.com/python-poetry/poetry-core/pull/874)). - Fix an issue where marker operations could result in invalid markers ([#875](https://github.com/python-poetry/poetry-core/pull/875)). ## [2.1.4] - 2025-08-05 ### Changed - Require `virtualenv<20.33` to work around an issue where Poetry uses the wrong Python version ([#10491](https://github.com/python-poetry/poetry/pull/10491)). - Improve the error messages for the validation of the `pyproject.toml` file ([#10471](https://github.com/python-poetry/poetry/pull/10471)). ### Fixed - Fix an issue where project plugins were installed even though `poetry install` was called with `--no-plugins` ([#10405](https://github.com/python-poetry/poetry/pull/10405)). - Fix an issue where dependency resolution failed for self-referential extras with duplicate dependencies ([#10488](https://github.com/python-poetry/poetry/pull/10488)). ### Docs - Clarify how to include files that were automatically excluded via VCS ignore settings ([#10442](https://github.com/python-poetry/poetry/pull/10442)). - Clarify the behavior of `poetry add` if no version constraint is explicitly specified ([#10445](https://github.com/python-poetry/poetry/pull/10445)). ## [2.1.3] - 2025-05-04 ### Changed - Require `importlib-metadata<8.7` for Python 3.9 because of a breaking change in importlib-metadata 8.7 ([#10374](https://github.com/python-poetry/poetry/pull/10374)). ### Fixed - Fix an issue where re-locking failed for incomplete multiple-constraints dependencies with explicit sources ([#10324](https://github.com/python-poetry/poetry/pull/10324)). - Fix an issue where the `--directory` option did not work if a plugin, which accesses the poetry instance during its activation, was installed ([#10352](https://github.com/python-poetry/poetry/pull/10352)). - Fix an issue where `poetry env activate -v` printed additional information to stdout instead of stderr so that the output could not be used as designed ([#10353](https://github.com/python-poetry/poetry/pull/10353)). - Fix an issue where the original error was not printed if building a git dependency failed ([#10366](https://github.com/python-poetry/poetry/pull/10366)). - Fix an issue where wheels for the wrong platform were installed in rare cases. ([#10361](https://github.com/python-poetry/poetry/pull/10361)). ### poetry-core ([`2.1.3`](https://github.com/python-poetry/poetry-core/releases/tag/2.1.3)) - Fix an issue where the union of specific inverse or partially inverse markers was not simplified ([#858](https://github.com/python-poetry/poetry-core/pull/858)). - Fix an issue where optional dependencies defined in the `project` section were treated as non-optional when a source was defined for them in the `tool.poetry` section ([#857](https://github.com/python-poetry/poetry-core/pull/857)). - Fix an issue where markers with `===` were not parsed correctly ([#860](https://github.com/python-poetry/poetry-core/pull/860)). - Fix an issue where local versions with upper case letters caused an error ([#859](https://github.com/python-poetry/poetry-core/pull/859)). - Fix an issue where `extra` markers with a value starting with "in" were not validated correctly ([#862](https://github.com/python-poetry/poetry-core/pull/862)). ## [2.1.2] - 2025-03-29 ### Changed - Improve performance of locking dependencies ([#10275](https://github.com/python-poetry/poetry/pull/10275)). ### Fixed - Fix an issue where markers were not locked correctly ([#10240](https://github.com/python-poetry/poetry/pull/10240)). - Fix an issue where the result of `poetry lock` was not deterministic ([#10276](https://github.com/python-poetry/poetry/pull/10276)). - Fix an issue where `poetry env activate` returned the wrong command for `tcsh` ([#10243](https://github.com/python-poetry/poetry/pull/10243)). - Fix an issue where `poetry env activate` returned the wrong command for `pwsh` on Linux ([#10256](https://github.com/python-poetry/poetry/pull/10256)). ### Docs - Update basic usage section to reflect new default layout ([#10203](https://github.com/python-poetry/poetry/pull/10203)). ### poetry-core ([`2.1.2`](https://github.com/python-poetry/poetry-core/releases/tag/2.1.2)) - Improve performance of marker operations ([#851](https://github.com/python-poetry/poetry-core/pull/851)). - Fix an issue where incorrect markers were calculated when removing parts covered by the project's Python constraint ([#841](https://github.com/python-poetry/poetry-core/pull/841), [#846](https://github.com/python-poetry/poetry-core/pull/846)). - Fix an issue where `extra` markers were not simplified ([#842](https://github.com/python-poetry/poetry-core/pull/842), [#845](https://github.com/python-poetry/poetry-core/pull/845), [#847](https://github.com/python-poetry/poetry-core/pull/847)). - Fix an issue where the intersection and union of markers was not deterministic ([#843](https://github.com/python-poetry/poetry-core/pull/843)). - Fix an issue where the intersection of `python_version` markers was not recognized as empty ([#849](https://github.com/python-poetry/poetry-core/pull/849)). - Fix an issue where `python_version` markers were not simplified ([#848](https://github.com/python-poetry/poetry-core/pull/848), [#851](https://github.com/python-poetry/poetry-core/pull/851)). - Fix an issue where Python constraints on a package were converted into invalid markers ([#853](https://github.com/python-poetry/poetry-core/pull/853)). ## [2.1.1] - 2025-02-16 ### Fixed - Fix an issue where `poetry env use python` does not choose the Python from the PATH ([#10187](https://github.com/python-poetry/poetry/pull/10187)). ### poetry-core ([`2.1.1`](https://github.com/python-poetry/poetry-core/releases/tag/2.1.1)) - Fix an issue where simplifying a `python_version` marker resulted in an invalid marker ([#838](https://github.com/python-poetry/poetry-core/pull/838)). ## [2.1.0] - 2025-02-15 ### Added - **Make `build` command build-system agnostic** ([#10059](https://github.com/python-poetry/poetry/pull/10059), [#10092](https://github.com/python-poetry/poetry/pull/10092)). - Add a `--config-settings` option to `poetry build` ([#10059](https://github.com/python-poetry/poetry/pull/10059)). - Add support for defining `config-settings` when building dependencies ([#10129](https://github.com/python-poetry/poetry/pull/10129)). - **Add (experimental) commands to manage Python installations** ([#10112](https://github.com/python-poetry/poetry/pull/10112)). - Use `findpython` to find the Python interpreters ([#10097](https://github.com/python-poetry/poetry/pull/10097)). - Add a `--no-truncate` option to `poetry show` ([#9580](https://github.com/python-poetry/poetry/pull/9580)). - Re-add support for passwords with empty usernames ([#10088](https://github.com/python-poetry/poetry/pull/10088)). - Add better error messages ([#10053](https://github.com/python-poetry/poetry/pull/10053), [#10065]( https://github.com/python-poetry/poetry/pull/10065), [#10126](https://github.com/python-poetry/poetry/pull/10126), [#10127](https://github.com/python-poetry/poetry/pull/10127), [#10132](https://github.com/python-poetry/poetry/pull/10132)). ### Changed - **`poetry new` defaults to "src" layout by default** ([#10135](https://github.com/python-poetry/poetry/pull/10135)). - Improve performance of locking dependencies ([#10111](https://github.com/python-poetry/poetry/pull/10111), [#10114](https://github.com/python-poetry/poetry/pull/10114), [#10138](https://github.com/python-poetry/poetry/pull/10138), [#10146](https://github.com/python-poetry/poetry/pull/10146)). - Deprecate adding sources without specifying `--priority` ([#10134](https://github.com/python-poetry/poetry/pull/10134)). ### Fixed - Fix an issue where global options were not handled correctly when positioned after command options ([#10021](https://github.com/python-poetry/poetry/pull/10021), [#10067](https://github.com/python-poetry/poetry/pull/10067), [#10128](https://github.com/python-poetry/poetry/pull/10128)). - Fix an issue where building a dependency from source failed because of a conflict between build-system dependencies that were not required for the target environment ([#10048](https://github.com/python-poetry/poetry/pull/10048)). - Fix an issue where `poetry init` was not able to find a package on PyPI while adding dependencies interactively ([#10055](https://github.com/python-poetry/poetry/pull/10055)). - Fix an issue where the `@latest` descriptor was incorrectly passed to the core requirement parser ([#10069](https://github.com/python-poetry/poetry/pull/10069)). - Fix an issue where Boolean environment variables set to `True` (in contrast to `true`) were interpreted as `false` ([#10080](https://github.com/python-poetry/poetry/pull/10080)). - Fix an issue where `poetry env activate` reported a misleading error message ([#10087](https://github.com/python-poetry/poetry/pull/10087)). - Fix an issue where adding an optional dependency with `poetry add --optional` would not correctly update the lock file ([#10076](https://github.com/python-poetry/poetry/pull/10076)). - Fix an issue where `pip` was not installed/updated before other dependencies resulting in a race condition ([#10102](https://github.com/python-poetry/poetry/pull/10102)). - Fix an issue where Poetry freezes when multiple threads attempt to unlock the `keyring` simultaneously ([#10062](https://github.com/python-poetry/poetry/pull/10062)). - Fix an issue where markers with extras were not locked correctly ([#10119](https://github.com/python-poetry/poetry/pull/10119)). - Fix an issue where self-referential extras were not resolved correctly ([#10106](https://github.com/python-poetry/poetry/pull/10106)). - Fix an issue where Poetry could not be run from a `zipapp` ([#10074](https://github.com/python-poetry/poetry/pull/10074)). - Fix an issue where installation failed with a permission error when using the system environment as a user without write access to system site packages ([#9014](https://github.com/python-poetry/poetry/pull/9014)). - Fix an issue where a version of a dependency that is not compatible with the project's python constraint was locked. ([#10141](https://github.com/python-poetry/poetry/pull/10141)). - Fix an issue where Poetry wrongly reported that the current project's supported Python range is not compatible with some of the required packages Python requirement ([#10157](https://github.com/python-poetry/poetry/pull/10157)). - Fix an issue where the requested extras of a dependency were ignored if the same dependency (with same extras) was specified in multiple groups ([#10158](https://github.com/python-poetry/poetry/pull/10158)). ### Docs - Sort commands by name in the CLI reference ([#10035](https://github.com/python-poetry/poetry/pull/10035)). - Add missing documentation for `env` commands ([#10027](https://github.com/python-poetry/poetry/pull/10027)). - Clarify that the `name` and `version` fields are always required if the `project` section is specified ([#10033](https://github.com/python-poetry/poetry/pull/10033)). - Add a note about restarting the shell for tab completion changes to take effect ([#10070](https://github.com/python-poetry/poetry/pull/10070)). - Fix the example for `project.gui-scripts` [#10121](https://github.com/python-poetry/poetry/pull/10121). - Explain how to include files as scripts in the project configuration ([#9572](https://github.com/python-poetry/poetry/pull/9572), [#10133](https://github.com/python-poetry/poetry/pull/10133)). - Add additional information on specifying required python versions ([#10104](https://github.com/python-poetry/poetry/pull/10104)). ### poetry-core ([`2.1.0`](https://github.com/python-poetry/poetry-core/releases/tag/2.1.0)) - Fix an issue where inclusive ordering with post releases was inconsistent with PEP 440 ([#379](https://github.com/python-poetry/poetry-core/pull/379)). - Fix an issue where invalid URI tokens in PEP 508 requirement strings were silently discarded ([#817](https://github.com/python-poetry/poetry-core/pull/817)). - Fix an issue where wrong markers were calculated when removing parts covered by the project's python constraint ([#824](https://github.com/python-poetry/poetry-core/pull/824)). - Fix an issue where optional dependencies that are not part of an extra were included in the wheel metadata ([#830](https://github.com/python-poetry/poetry-core/pull/830)). - Fix an issue where the `__pycache__` directory and `*.pyc` files were included in sdists and wheels ([#835](https://github.com/python-poetry/poetry-core/pull/835)). ## [2.0.1] - 2025-01-11 ### Added - Add support for `poetry search` in legacy sources ([#9949](https://github.com/python-poetry/poetry/pull/9949)). - Add a message in the `poetry source show` output when PyPI is implicitly enabled ([#9974](https://github.com/python-poetry/poetry/pull/9974)). ### Changed - Improve performance for merging markers from overrides at the end of dependency resolution ([#10018](https://github.com/python-poetry/poetry/pull/10018)). ### Fixed - Fix an issue where `poetry sync` did not remove packages that were not requested ([#9946](https://github.com/python-poetry/poetry/pull/9946)). - Fix an issue where `poetry check` failed even though there were just warnings and add a `--strict` option to fail on warnings ([#9983](https://github.com/python-poetry/poetry/pull/9983)). - Fix an issue where `poetry update`, `poetry add` and `poetry remove` with `--only` uninstalled packages from other groups ([#10014](https://github.com/python-poetry/poetry/pull/10014)). - Fix an issue where `poetry update`, `poetry add` and `poetry remove` uninstalled all extra packages ([#10016](https://github.com/python-poetry/poetry/pull/10016)). - Fix an issue where `poetry self update` did not recognize Poetry's own environment ([#9995](https://github.com/python-poetry/poetry/pull/9995)). - Fix an issue where read-only system site-packages were not considered when loading an environment with system site-packages ([#9942](https://github.com/python-poetry/poetry/pull/9942)). - Fix an issue where an error message in `poetry install` started with `Warning:` instead of `Error:` ([#9945](https://github.com/python-poetry/poetry/pull/9945)). - Fix an issue where `Command.set_poetry`, which is used by plugins, was removed ([#9981](https://github.com/python-poetry/poetry/pull/9981)). - Fix an issue where the help text of `poetry build --clean` showed a malformed short option instead of the description ([#9994](https://github.com/python-poetry/poetry/pull/9994)). ### Docs - Add a FAQ entry for the migration from Poetry-specific fields to the `project` section ([#9996](https://github.com/python-poetry/poetry/pull/9996)). - Fix examples for `project.readme` and `project.urls` ([#9948](https://github.com/python-poetry/poetry/pull/9948)). - Add a warning that package sources are a Poetry-specific feature that is not included in core metadata ([#9935](https://github.com/python-poetry/poetry/pull/9935)). - Replace `poetry install --sync` with `poetry sync` in the section about synchronizing dependencies ([#9944](https://github.com/python-poetry/poetry/pull/9944)). - Replace `poetry shell` with `poetry env activate` in the basic usage section ([#9963](https://github.com/python-poetry/poetry/pull/9963)). - Mention that `project.name` is always required when the `project` section is used ([#9989](https://github.com/python-poetry/poetry/pull/9989)). - Fix the constraint of `poetry-plugin-export` in the section about `poetry export` ([#9954](https://github.com/python-poetry/poetry/pull/9954)). ### poetry-core ([`2.0.1`](https://github.com/python-poetry/poetry-core/releases/tag/2.0.1)) - Replace the deprecated core metadata field `Home-page` with `Project-URL: Homepage` ([#807](https://github.com/python-poetry/poetry-core/pull/807)). - Fix an issue where includes from `tool.poetry.packages` without a specified `format` were not initialized with the default value resulting in a `KeyError` ([#805](https://github.com/python-poetry/poetry-core/pull/805)). - Fix an issue where some `project.urls` entries were not processed correctly resulting in a `KeyError` ([#807](https://github.com/python-poetry/poetry-core/pull/807)). - Fix an issue where dynamic `project.dependencies` via `tool.poetry.dependencies` were ignored if `project.optional-dependencies` were defined ([#811](https://github.com/python-poetry/poetry-core/pull/811)). ## [2.0.0] - 2025-01-05 ### Added - **Add support for the `project` section in the `pyproject.toml` file according to PEP 621** ([#9135](https://github.com/python-poetry/poetry/pull/9135), [#9917](https://github.com/python-poetry/poetry/pull/9917)). - **Add support for defining Poetry plugins that are required by the project and automatically installed if not present** ([#9547](https://github.com/python-poetry/poetry/pull/9547)). - **Lock resulting markers and groups and add a `installer.re-resolve` option (default: `true`) to allow installation without re-resolving** ([#9427](https://github.com/python-poetry/poetry/pull/9427)). - Add a `--local-version` option to `poetry build` ([#9064](https://github.com/python-poetry/poetry/pull/9064)). - Add a `--clean` option to `poetry build` ([#9067](https://github.com/python-poetry/poetry/pull/9067)). - Add FIPS support for `poetry publish` ([#9101](https://github.com/python-poetry/poetry/pull/9101)). - Add the option to use `poetry new` interactively and configure more fields ([#9101](https://github.com/python-poetry/poetry/pull/9101)). - Add a config option `installer.only-binary` to enforce the use of binary distribution formats ([#9150](https://github.com/python-poetry/poetry/pull/9150)). - Add backend support for legacy repository search ([#9132](https://github.com/python-poetry/poetry/pull/9132)). - Add support to resume downloads from connection resets ([#9422](https://github.com/python-poetry/poetry/pull/9422)). - Add the option to define a constraint for the required Poetry version to manage the project ([#9547](https://github.com/python-poetry/poetry/pull/9547)). - Add an `--all-groups` option to `poetry install` ([#9744](https://github.com/python-poetry/poetry/pull/9744)). - Add an `poetry env activate` command as replacement of `poetry shell` ([#9763](https://github.com/python-poetry/poetry/pull/9763)). - Add a `--markers` option to `poetry add` to add a dependency with markers ([#9814](https://github.com/python-poetry/poetry/pull/9814)). - Add a `--migrate` option to `poetry config` to migrate outdated configs ([#9830](https://github.com/python-poetry/poetry/pull/9830)). - Add a `--project` option to search the `pyproject.toml` file in another directory without switching the directory ([#9831](https://github.com/python-poetry/poetry/pull/9831)). - Add support for shortened hashes to define git dependencies ([#9748](https://github.com/python-poetry/poetry/pull/9748)). - Add partial support for conflicting extras ([#9553](https://github.com/python-poetry/poetry/pull/9553)). - Add a `poetry sync` command as replacement of `poetry install --sync` ([#9801](https://github.com/python-poetry/poetry/pull/9801)). ### Changed - **Change the default behavior of `poetry lock` to `--no-update` and introduce a `--regenerate` option for the old default behavior** ([#9327](https://github.com/python-poetry/poetry/pull/9327)). - **Remove the dependency on `poetry-plugin-export` so that `poetry export` is not included per default** ([#5980](https://github.com/python-poetry/poetry/pull/5980)). - **Outsource `poetry shell` into `poetry-plugin-shell`** ([#9763](https://github.com/python-poetry/poetry/pull/9763)). - **Change the interface of `poetry add --optional` to require an extra the optional dependency is added to** ([#9135](https://github.com/python-poetry/poetry/pull/9135)). - **Actually switch the directory when using `--directory`/`-C`** ([#9831](https://github.com/python-poetry/poetry/pull/9831)). - **Drop support for Python 3.8** ([#9692](https://github.com/python-poetry/poetry/pull/9692)). - Rename `experimental.system-git-client` to `experimental.system-git` ([#9787](https://github.com/python-poetry/poetry/pull/9787), [#9795](https://github.com/python-poetry/poetry/pull/9795)). - Replace `virtualenvs.prefer-active-python` by the inverse setting `virtualenvs.use-poetry-python` and prefer the active Python by default ([#9786](https://github.com/python-poetry/poetry/pull/9786)). - Deprecate several fields in the `tool.poetry` section in favor of the respective fields in the `project` section in the `pyproject.toml` file ([#9135](https://github.com/python-poetry/poetry/pull/9135)). - Deprecate `poetry install --sync` in favor of `poetry sync` ([#9801](https://github.com/python-poetry/poetry/pull/9801)). - Upgrade the warning if the current project cannot be installed to an error ([#9333](https://github.com/python-poetry/poetry/pull/9333)). - Remove special handling for `platformdirs 2.0` macOS config directory ([#8916](https://github.com/python-poetry/poetry/pull/8916)). - Tweak PEP 517 builds ([#9094](https://github.com/python-poetry/poetry/pull/9094)). - Use Poetry instead of pip to manage dependencies in isolated build environments ([#9168](https://github.com/python-poetry/poetry/pull/9168), [#9227](https://github.com/python-poetry/poetry/pull/9227)). - Trust empty `Requires-Dist` with modern metadata ([#9078](https://github.com/python-poetry/poetry/pull/9078)). - Do PEP 517 builds instead of parsing `setup.py` to determine dependencies ([#9099](https://github.com/python-poetry/poetry/pull/9099)). - Drop support for reading lock files prior version 1.0 (created with Poetry prior 1.1) ([#9345](https://github.com/python-poetry/poetry/pull/9345)). - Default to `>=` instead of `^` for the Python requirement when initializing a new project ([#9558](https://github.com/python-poetry/poetry/pull/9558)). - Limit `build-system` to the current major version of `poetry-core` when initializing a new project ([#9812](https://github.com/python-poetry/poetry/pull/9812)). - Remove pip-based installation, i.e. `installer.modern-installation = false` ([#9392](https://github.com/python-poetry/poetry/pull/9392)). - Remove `virtualenvs.options.no-setuptools` config option and never include `setuptools` per default ([#9331](https://github.com/python-poetry/poetry/pull/9331)). - Rename exceptions to have an `Error` suffix ([#9705](https://github.com/python-poetry/poetry/pull/9705)). - Remove deprecated CLI options and methods and revoke the deprecation of `--dev` ([#9732](https://github.com/python-poetry/poetry/pull/9732)). - Ignore installed packages during dependency resolution ([#9851](https://github.com/python-poetry/poetry/pull/9851)). - Improve the error message on upload failure ([#9701](https://github.com/python-poetry/poetry/pull/9701)). - Improve the error message if the current project cannot be installed to include another root cause ([#9651](https://github.com/python-poetry/poetry/pull/9651)). - Improve the output of `poetry show ` ([#9750](https://github.com/python-poetry/poetry/pull/9750)). - Improve the error message for build errors ([#9870](https://github.com/python-poetry/poetry/pull/9870)). - Improve the error message when trying to remove a package from a project without any dependencies ([#9918](https://github.com/python-poetry/poetry/pull/9918)). - Drop the direct dependency on `crashtest` ([#9108](https://github.com/python-poetry/poetry/pull/9108)). - Require `keyring>=23.3.1` ([#9167](https://github.com/python-poetry/poetry/pull/9167)). - Require `build>=1.2.1` ([#9283](https://github.com/python-poetry/poetry/pull/9283)). - Require `dulwich>=0.22.6` ([#9748](https://github.com/python-poetry/poetry/pull/9748)). ### Fixed - Fix an issue where git dependencies with extras could only be cloned if a branch was specified explicitly ([#7028](https://github.com/python-poetry/poetry/pull/7028)). - Fix an issue where `poetry env remove` failed if `virtualenvs.in-project` was set to `true` ([#9118](https://github.com/python-poetry/poetry/pull/9118)). - Fix an issue where locking packages with a digit at the end of the name and non-standard sdist names failed ([#9189](https://github.com/python-poetry/poetry/pull/9189)). - Fix an issue where credentials where not passed when trying to download an URL dependency ([#9202](https://github.com/python-poetry/poetry/pull/9202)). - Fix an issue where using uncommon group names with `poetry add` resulted in a broken `pyproject.toml` ([#9277](https://github.com/python-poetry/poetry/pull/9277)). - Fix an issue where an inconsistent entry regarding the patch version of Python was kept in `envs.toml` ([#9286](https://github.com/python-poetry/poetry/pull/9286)). - Fix an issue where relative paths were not resolved properly when using `poetry build --directory` ([#9433](https://github.com/python-poetry/poetry/pull/9433)). - Fix an issue where unrequested extras were not uninstalled when running `poetry install` without an existing lock file ([#9345](https://github.com/python-poetry/poetry/pull/9345)). - Fix an issue where the `poetry-check` pre-commit hook did not trigger if only `poetry.lock` has changed ([#9504](https://github.com/python-poetry/poetry/pull/9504)). - Fix an issue where files (rather than directories) could not be added as single page source ([#9166](https://github.com/python-poetry/poetry/pull/9166)). - Fix an issue where invalid constraints were generated when adding a package with a local version specifier ([#9603](https://github.com/python-poetry/poetry/pull/9603)). - Fix several encoding warnings ([#8893](https://github.com/python-poetry/poetry/pull/8893)). - Fix an issue where `virtualenvs.prefer-active-python` was not respected ([#9278](https://github.com/python-poetry/poetry/pull/9278)). - Fix an issue where the line endings of the lock file were changed ([#9468](https://github.com/python-poetry/poetry/pull/9468)). - Fix an issue where installing multiple dependencies from the same git repository failed sporadically due to a race condition ([#9658](https://github.com/python-poetry/poetry/pull/9658)). - Fix an issue where installing multiple dependencies from forked monorepos failed sporadically due to a race condition ([#9723](https://github.com/python-poetry/poetry/pull/9723)). - Fix an issue where an extra package was not installed if it is required by multiple extras ([#9700](https://github.com/python-poetry/poetry/pull/9700)). - Fix an issue where a `direct_url.json` with vcs URLs not compliant with PEP 610 was written ([#9007](https://github.com/python-poetry/poetry/pull/9007)). - Fix an issue where other files than wheels were recognized as wheels ([#9770](https://github.com/python-poetry/poetry/pull/9770)). - Fix an issue where `installer.max-workers` was ignored for the implicit PyPI source ([#9815](https://github.com/python-poetry/poetry/pull/9815)). - Fix an issue where local settings (from `poetry.toml`) were ignored for the implicit PyPI source ([#9816](https://github.com/python-poetry/poetry/pull/9816)). - Fix an issue where different `dulwich` versions resulted in different hashes for a git dependency from a tag ([#9849](https://github.com/python-poetry/poetry/pull/9849)). - Fix an issue where installing a yanked package with no dependencies failed with an `IndexError` ([#9505](https://github.com/python-poetry/poetry/pull/9505)). - Fix an issue where a package could not be added from a source that required an empty password ([#9850](https://github.com/python-poetry/poetry/pull/9850)). - Fix an issue where setting `allow-prereleases = false` still allowed pre-releases if no other solution was found ([#9798](https://github.com/python-poetry/poetry/pull/9798)). - Fix an issue where the wrong environment was used for checking if an installed package is from system site packages ([#9861](https://github.com/python-poetry/poetry/pull/9861)). - Fix an issue where build errors from builds to retrieve metadata information were hidden ([#9870](https://github.com/python-poetry/poetry/pull/9870)). - Fix an issue where `poetry check` falsely reported that an invalid source "pypi" is referenced in dependencies ([#9475](https://github.com/python-poetry/poetry/pull/9475)). - Fix an issue where `poetry install --sync` tried to uninstall system site packages if the virtual environment was created with `virtualenvs.options.system-site-packages = true` ([#9863](https://github.com/python-poetry/poetry/pull/9863)). - Fix an issue where HTTP streaming requests were not closed properly when not completely consumed ([#9899](https://github.com/python-poetry/poetry/pull/9899)). ### Docs - Add information about getting test coverage in the contribution guide ([#9726](https://github.com/python-poetry/poetry/pull/9726)). - Mention `pre-commit-update` as an alternative to `pre-commit autoupdate` ([#9716](https://github.com/python-poetry/poetry/pull/9716)). - Improve the explanation of `exclude` and `include` ([#9734](https://github.com/python-poetry/poetry/pull/9734)). - Add information about compatible release requirements, i.e. `~=` ([#9783](https://github.com/python-poetry/poetry/pull/9783)). - Add documentation for using a build script to build extension modules ([#9864](https://github.com/python-poetry/poetry/pull/9864)). ### poetry-core ([`2.0.0`](https://github.com/python-poetry/poetry-core/releases/tag/2.0.0)) - Add support for non PEP440 compliant version in the `platform_release` marker ([#722](https://github.com/python-poetry/poetry-core/pull/722)). - Add support for string comparisons with `in` / `not in` in generic constraints ([#722](https://github.com/python-poetry/poetry-core/pull/722)). - Add support for script files that are generated by a build script ([#710](https://github.com/python-poetry/poetry-core/pull/710)). - Add support for `SOURCE_DATE_EPOCH` when building packages ([#766](https://github.com/python-poetry/poetry-core/pull/766), [#781](https://github.com/python-poetry/poetry-core/pull/781)). - Create `METADATA` files with version 2.3 instead of 2.2 ([#707](https://github.com/python-poetry/poetry-core/pull/707)). - Remove support for `x` in version constraints ([#770](https://github.com/python-poetry/poetry-core/pull/770)). - Remove support for scripts with extras ([#708](https://github.com/python-poetry/poetry-core/pull/708)). - Remove deprecated features and interfaces ([#702](https://github.com/python-poetry/poetry-core/pull/702), [#769](https://github.com/python-poetry/poetry-core/pull/769)). - Deprecate `tool.poetry.dev-dependencies` in favor of `tool.poetry.group.dev.dependencies` ([#754](https://github.com/python-poetry/poetry-core/pull/754)). - Fix an issue where the `platlib` directory of the wrong Python was used ([#726](https://github.com/python-poetry/poetry-core/pull/726)). - Fix an issue where building a wheel in a nested output directory results in an error ([#762](https://github.com/python-poetry/poetry-core/pull/762)). - Fix an issue where `+` was not allowed in git URL paths ([#765](https://github.com/python-poetry/poetry-core/pull/765)). - Fix an issue where the temporary directory was not cleaned up on error ([#775](https://github.com/python-poetry/poetry-core/pull/775)). - Fix an issue where the regular expression for author names was too restrictive ([#517](https://github.com/python-poetry/poetry-core/pull/517)). - Fix an issue where basic auth http(s) credentials could not be parsed ([#791](https://github.com/python-poetry/poetry-core/pull/791)). ## [1.8.5] - 2024-12-06 ### Changed - Require `pkginfo>=1.12` to fix an issue with an unknown metadata version 2.4 ([#9888](https://github.com/python-poetry/poetry/pull/9888)). - Do not fail if the unknown metadata version is only a minor version update ([#9888](https://github.com/python-poetry/poetry/pull/9888)). ## [1.8.4] - 2024-10-14 ### Added - **Add official support for Python 3.13** ([#9523](https://github.com/python-poetry/poetry/pull/9523)). ### Changed - Require `virtualenv>=20.26.6` to mitigate potential command injection when running `poetry shell` in untrusted projects ([#9757](https://github.com/python-poetry/poetry/pull/9757)). ### poetry-core ([`1.9.1`](https://github.com/python-poetry/poetry-core/releases/tag/1.9.1)) - Add `3.13` to the list of available Python versions ([#747](https://github.com/python-poetry/poetry-core/pull/747)). ## [1.8.3] - 2024-05-08 ### Added - Add support for untagged CPython builds with versions ending with a `+` ([#9207](https://github.com/python-poetry/poetry/pull/9207)). ### Changed - Require `pkginfo>=1.10` to ensure support for packages with metadata version 2.3 ([#9130](https://github.com/python-poetry/poetry/pull/9130)). - Improve locking on FIPS systems ([#9152](https://github.com/python-poetry/poetry/pull/9152)). ### Fixed - Fix an issue where unrecognized package metadata versions silently resulted in empty dependencies ([#9203](https://github.com/python-poetry/poetry/pull/9203), [#9226](https://github.com/python-poetry/poetry/pull/9226)). - Fix an issue where trailing slashes in git URLs where not handled correctly ([#9205](https://github.com/python-poetry/poetry/pull/9205)). - Fix an issue where `poetry self` commands printed a warning that the current project cannot be installed ([#9302](https://github.com/python-poetry/poetry/pull/9302)). - Fix an issue where `poetry install` sporadically failed with a `KeyError` due to a race condition ([#9335](https://github.com/python-poetry/poetry/pull/9335)). ### Docs - Fix incorrect information about `poetry shell` ([#9060](https://github.com/python-poetry/poetry/pull/9060)). - Add a git subdirectory example to `poetry add` ([#9080](https://github.com/python-poetry/poetry/pull/9080)). - Mention interactive credential configuration ([#9074](https://github.com/python-poetry/poetry/pull/9074)). - Add notes for optional advanced installation steps ([#9098](https://github.com/python-poetry/poetry/pull/9098)). - Add reference to configuration credentials in documentation of poetry `publish` ([#9110](https://github.com/python-poetry/poetry/pull/9110)). - Improve documentation for configuring credentials via environment variables ([#9121](https://github.com/python-poetry/poetry/pull/9121)). - Remove misleading wording around virtual environments ([#9213](https://github.com/python-poetry/poetry/pull/9213)). - Remove outdated advice regarding seeding keyring backends ([#9164](https://github.com/python-poetry/poetry/pull/9164)). - Add a `pyproject.toml` example for a dependency with multiple extras ([#9138](https://github.com/python-poetry/poetry/pull/9138)). - Clarify help of `poetry add` ([#9230](https://github.com/python-poetry/poetry/pull/9230)). - Add a note how to configure credentials for TestPyPI for `poetry publish` ([#9255](https://github.com/python-poetry/poetry/pull/9255)). - Fix information about the `--readme` option in `poetry new` ([#9260](https://github.com/python-poetry/poetry/pull/9260)). - Clarify what is special about the Python constraint in `dependencies` ([#9256](https://github.com/python-poetry/poetry/pull/9256)). - Update how to uninstall plugins via `pipx` ([#9320](https://github.com/python-poetry/poetry/pull/9320)). ## [1.8.2] - 2024-03-02 ### Fixed - Harden `lazy-wheel` error handling if the index server is behaving badly in an unexpected way ([#9051](https://github.com/python-poetry/poetry/pull/9051)). - Improve `lazy-wheel` error handling if the index server does not handle HTTP range requests correctly ([#9082](https://github.com/python-poetry/poetry/pull/9082)). - Improve `lazy-wheel` error handling if the index server pretends to support HTTP range requests but does not respect them ([#9084](https://github.com/python-poetry/poetry/pull/9084)). - Improve `lazy-wheel` to allow redirects for HEAD requests ([#9087](https://github.com/python-poetry/poetry/pull/9087)). - Improve debug logging for `lazy-wheel` errors ([#9059](https://github.com/python-poetry/poetry/pull/9059)). - Fix an issue where the hash of a metadata file could not be calculated correctly due to an encoding issue ([#9049](https://github.com/python-poetry/poetry/pull/9049)). - Fix an issue where `poetry add` failed in non-package mode if no project name was set ([#9046](https://github.com/python-poetry/poetry/pull/9046)). - Fix an issue where a hint to non-package mode was not compliant with the final name of the setting ([#9073](https://github.com/python-poetry/poetry/pull/9073)). ## [1.8.1] - 2024-02-26 ### Fixed - Update the minimum required version of `packaging` ([#9031](https://github.com/python-poetry/poetry/pull/9031)). - Handle unexpected responses from servers that do not support HTTP range requests with negative offsets more robust ([#9030](https://github.com/python-poetry/poetry/pull/9030)). ### Docs - Rename `master` branch to `main` ([#9022](https://github.com/python-poetry/poetry/pull/9022)). ## [1.8.0] - 2024-02-25 ### Added - **Add a `non-package` mode for use cases where Poetry is only used for dependency management** ([#8650](https://github.com/python-poetry/poetry/pull/8650)). - **Add support for PEP 658 to fetch metadata without having to download wheels** ([#5509](https://github.com/python-poetry/poetry/pull/5509)). - **Add a `lazy-wheel` config option (default: `true`) to reduce wheel downloads during dependency resolution** ([#8815](https://github.com/python-poetry/poetry/pull/8815), [#8941](https://github.com/python-poetry/poetry/pull/8941)). - Improve performance of dependency resolution by using shallow copies instead of deep copies ([#8671](https://github.com/python-poetry/poetry/pull/8671)). - `poetry check` validates that no unknown sources are referenced in dependencies ([#8709](https://github.com/python-poetry/poetry/pull/8709)). - Add archive validation during installation for further hash algorithms ([#8851](https://github.com/python-poetry/poetry/pull/8851)). - Add a `to` key in `tool.poetry.packages` to allow custom subpackage names ([#8791](https://github.com/python-poetry/poetry/pull/8791)). - Add a config option to disable `keyring` ([#8910](https://github.com/python-poetry/poetry/pull/8910)). - Add a `--sync` option to `poetry update` ([#8931](https://github.com/python-poetry/poetry/pull/8931)). - Add an `--output` option to `poetry build` ([#8828](https://github.com/python-poetry/poetry/pull/8828)). - Add a `--dist-dir` option to `poetry publish` ([#8828](https://github.com/python-poetry/poetry/pull/8828)). ### Changed - **The implicit PyPI source is disabled if at least one primary source is configured** ([#8771](https://github.com/python-poetry/poetry/pull/8771)). - **Deprecate source priority `default`** ([#8771](https://github.com/python-poetry/poetry/pull/8771)). - **Upgrade the warning about an inconsistent lockfile to an error** ([#8737](https://github.com/python-poetry/poetry/pull/8737)). - Deprecate setting `installer.modern-installation` to `false` ([#8988](https://github.com/python-poetry/poetry/pull/8988)). - Drop support for `pip<19` ([#8894](https://github.com/python-poetry/poetry/pull/8894)). - Require `requests-toolbelt>=1` ([#8680](https://github.com/python-poetry/poetry/pull/8680)). - Allow `platformdirs` 4.x ([#8668](https://github.com/python-poetry/poetry/pull/8668)). - Allow and require `xattr` 1.x on macOS ([#8801](https://github.com/python-poetry/poetry/pull/8801)). - Improve venv shell activation in `fish` ([#8804](https://github.com/python-poetry/poetry/pull/8804)). - Rename `system` to `base` in output of `poetry env info` ([#8832](https://github.com/python-poetry/poetry/pull/8832)). - Use pretty name in output of `poetry version` ([#8849](https://github.com/python-poetry/poetry/pull/8849)). - Improve error handling for invalid entries in `tool.poetry.scripts` ([#8898](https://github.com/python-poetry/poetry/pull/8898)). - Improve verbose output for dependencies with extras during dependency resolution ([#8834](https://github.com/python-poetry/poetry/pull/8834)). - Improve message about an outdated lockfile ([#8962](https://github.com/python-poetry/poetry/pull/8962)). ### Fixed - Fix an issue where `poetry shell` failed when Python has been installed with MSYS2 ([#8644](https://github.com/python-poetry/poetry/pull/8644)). - Fix an issue where Poetry commands failed in a terminal with a non-UTF-8 encoding ([#8608](https://github.com/python-poetry/poetry/pull/8608)). - Fix an issue where a missing project name caused an incomprehensible error message ([#8691](https://github.com/python-poetry/poetry/pull/8691)). - Fix an issue where Poetry failed to install an `sdist` path dependency ([#8682](https://github.com/python-poetry/poetry/pull/8682)). - Fix an issue where `poetry install` failed because an unused extra was not available ([#8548](https://github.com/python-poetry/poetry/pull/8548)). - Fix an issue where `poetry install --sync` did not remove an unrequested extra ([#8621](https://github.com/python-poetry/poetry/pull/8621)). - Fix an issue where `poetry init` did not allow specific characters in the author field ([#8779](https://github.com/python-poetry/poetry/pull/8779)). - Fix an issue where Poetry could not download `sdists` from misconfigured servers ([#8701](https://github.com/python-poetry/poetry/pull/8701)). - Fix an issue where metadata of sdists that call CLI tools of their build requirements could not be determined ([#8827](https://github.com/python-poetry/poetry/pull/8827)). - Fix an issue where Poetry failed to use the currently activated environment ([#8831](https://github.com/python-poetry/poetry/pull/8831)). - Fix an issue where `poetry shell` failed in `zsh` if a space was in the venv path ([#7245](https://github.com/python-poetry/poetry/pull/7245)). - Fix an issue where scripts with extras could not be installed ([#8900](https://github.com/python-poetry/poetry/pull/8900)). - Fix an issue where explicit sources where not propagated correctly ([#8835](https://github.com/python-poetry/poetry/pull/8835)). - Fix an issue where debug prints where swallowed when using a build script ([#8760](https://github.com/python-poetry/poetry/pull/8760)). - Fix an issue where explicit sources of locked dependencies where not propagated correctly ([#8948](https://github.com/python-poetry/poetry/pull/8948)). - Fix an issue where Poetry's own environment was falsely identified as system environment ([#8970](https://github.com/python-poetry/poetry/pull/8970)). - Fix an issue where dependencies from a `setup.py` were ignored silently ([#9000](https://github.com/python-poetry/poetry/pull/9000)). - Fix an issue where environment variables for `virtualenv.options` were ignored ([#9015](https://github.com/python-poetry/poetry/pull/9015)). - Fix an issue where `virtualenvs.options.no-pip` and `virtualenvs.options.no-setuptools` were not normalized ([#9015](https://github.com/python-poetry/poetry/pull/9015)). ### Docs - Replace deprecated `--no-dev` with `--without dev` in the FAQ ([#8659](https://github.com/python-poetry/poetry/pull/8659)). - Recommend `poetry-check` instead of the deprecated `poetry-lock` pre-commit hook ([#8675](https://github.com/python-poetry/poetry/pull/8675)). - Clarify the names of the environment variables to provide credentials for repositories ([#8782](https://github.com/python-poetry/poetry/pull/8782)). - Add note how to install several version of Poetry in parallel ([#8814](https://github.com/python-poetry/poetry/pull/8814)). - Improve description of `poetry show --why` ([#8817](https://github.com/python-poetry/poetry/pull/8817)). - Improve documentation of `poetry update` ([#8706](https://github.com/python-poetry/poetry/pull/8706)). - Add a warning about passing variables that may start with a hyphen via command line ([#8850](https://github.com/python-poetry/poetry/pull/8850)). - Mention that the virtual environment in which Poetry itself is installed should not be activated ([#8833](https://github.com/python-poetry/poetry/pull/8833)). - Add note about `poetry run` and externally managed environments ([#8748](https://github.com/python-poetry/poetry/pull/8748)). - Update FAQ entry about `tox` for `tox` 4.x ([#8658](https://github.com/python-poetry/poetry/pull/8658)). - Fix documentation for default `format` option for `include` and `exclude` value ([#8852](https://github.com/python-poetry/poetry/pull/8852)). - Add note about `tox` and configured credentials ([#8888](https://github.com/python-poetry/poetry/pull/8888)). - Add note and link how to install `pipx` ([#8878](https://github.com/python-poetry/poetry/pull/8878)). - Fix examples for `poetry add` with git dependencies over ssh ([#8911](https://github.com/python-poetry/poetry/pull/8911)). - Remove reference to deprecated scripts extras feature ([#8903](https://github.com/python-poetry/poetry/pull/8903)). - Change examples to prefer `--only main` instead of `--without dev` ([#8921](https://github.com/python-poetry/poetry/pull/8921)). - Mention that the `develop` attribute is a Poetry-specific feature and not propagated to other tools ([#8971](https://github.com/python-poetry/poetry/pull/8971)). - Fix examples for adding supplemental and secondary sources ([#8953](https://github.com/python-poetry/poetry/pull/8953)). - Add PyTorch example for explicit sources ([#9006](https://github.com/python-poetry/poetry/pull/9006)). ### poetry-core ([`1.9.0`](https://github.com/python-poetry/poetry-core/releases/tag/1.9.0)) - **Deprecate scripts that depend on extras** ([#690](https://github.com/python-poetry/poetry-core/pull/690)). - Add support for path dependencies that do not define a build system ([#675](https://github.com/python-poetry/poetry-core/pull/675)). - Update list of supported licenses ([#659](https://github.com/python-poetry/poetry-core/pull/659), [#669](https://github.com/python-poetry/poetry-core/pull/669), [#678](https://github.com/python-poetry/poetry-core/pull/678), [#694](https://github.com/python-poetry/poetry-core/pull/694)). - Rework list of files included in build artifacts ([#666](https://github.com/python-poetry/poetry-core/pull/666)). - Fix an issue where insignificant errors were printed if the working directory is not inside a git repository ([#684](https://github.com/python-poetry/poetry-core/pull/684)). - Fix an issue where the project's directory was not recognized as git repository on Windows due to an encoding issue ([#685](https://github.com/python-poetry/poetry-core/pull/685)). ## [1.7.1] - 2023-11-16 ### Fixed - Fix an issue where sdists that call CLI tools of their build requirements could not be installed ([#8630](https://github.com/python-poetry/poetry/pull/8630)). - Fix an issue where sdists with symlinks could not be installed due to a broken tarfile datafilter ([#8649](https://github.com/python-poetry/poetry/pull/8649)). - Fix an issue where `poetry init` failed when trying to add dependencies ([#8655](https://github.com/python-poetry/poetry/pull/8655)). - Fix an issue where `poetry install` failed if `virtualenvs.create` was set to `false` ([#8672](https://github.com/python-poetry/poetry/pull/8672)). ## [1.7.0] - 2023-11-03 ### Added - **Add official support for Python 3.12** ([#7803](https://github.com/python-poetry/poetry/pull/7803), [#8544](https://github.com/python-poetry/poetry/pull/8544)). - **Print a future warning that `poetry-plugin-export` will not be installed by default anymore** ([#8562](https://github.com/python-poetry/poetry/pull/8562)). - Add `poetry-install` pre-commit hook ([#8327](https://github.com/python-poetry/poetry/pull/8327)). - Add `--next-phase` option to `poetry version` ([#8089](https://github.com/python-poetry/poetry/pull/8089)). - Print a warning when overwriting files from another package at installation ([#8386](https://github.com/python-poetry/poetry/pull/8386)). - Print a warning if the current project cannot be installed ([#8369](https://github.com/python-poetry/poetry/pull/8369)). - Report more details on build backend exceptions ([#8464](https://github.com/python-poetry/poetry/pull/8464)). ### Changed - Set Poetry as `user-agent` for all HTTP requests ([#8394](https://github.com/python-poetry/poetry/pull/8394)). - Do not install `setuptools` per default in Python 3.12 ([#7803](https://github.com/python-poetry/poetry/pull/7803)). - Do not install `wheel` per default ([#7803](https://github.com/python-poetry/poetry/pull/7803)). - Remove `setuptools` and `wheel` when running `poetry install --sync` if they are not required by the project ([#8600](https://github.com/python-poetry/poetry/pull/8600)). - Improve error message about PEP-517 support ([#8463](https://github.com/python-poetry/poetry/pull/8463)). - Improve `keyring` handling ([#8227](https://github.com/python-poetry/poetry/pull/8227)). - Read the `description` field when extracting metadata from `setup.py` files ([#8545](https://github.com/python-poetry/poetry/pull/8545)). ### Fixed - **Fix an issue where dependencies of inactive extras were locked and installed** ([#8399](https://github.com/python-poetry/poetry/pull/8399)). - **Fix an issue where build requirements were not installed due to a race condition in the artifact cache** ([#8517](https://github.com/python-poetry/poetry/pull/8517)). - Fix an issue where packages included in the system site packages were installed even though `virtualenvs.options.system-site-packages` was set ([#8359](https://github.com/python-poetry/poetry/pull/8359)). - Fix an issue where git dependencies' submodules with relative URLs were handled incorrectly ([#8020](https://github.com/python-poetry/poetry/pull/8020)). - Fix an issue where a failed installation of build dependencies was not noticed directly ([#8479](https://github.com/python-poetry/poetry/pull/8479)). - Fix an issue where `poetry shell` did not work completely with `nushell` ([#8478](https://github.com/python-poetry/poetry/pull/8478)). - Fix an issue where a confusing error messages was displayed when running `poetry config pypi-token.pypi` without a value ([#8502](https://github.com/python-poetry/poetry/pull/8502)). - Fix an issue where a cryptic error message is printed if there is no metadata entry in the lockfile ([#8523](https://github.com/python-poetry/poetry/pull/8523)). - Fix an issue with the encoding with special characters in the virtualenv's path ([#8565](https://github.com/python-poetry/poetry/pull/8565)). - Fix an issue where the connection pool size was not adjusted to the number of workers ([#8559](https://github.com/python-poetry/poetry/pull/8559)). ### Docs - Improve the wording regarding a project's supported Python range ([#8423](https://github.com/python-poetry/poetry/pull/8423)). - Make `pipx` the preferred (first mentioned) installation method ([#8090](https://github.com/python-poetry/poetry/pull/8090)). - Add a warning about `poetry self` on Windows ([#8090](https://github.com/python-poetry/poetry/pull/8090)). - Fix example for `poetry add` with a git dependency ([#8438](https://github.com/python-poetry/poetry/pull/8438)). - Add information about auto-included files in wheels and sdist ([#8555](https://github.com/python-poetry/poetry/pull/8555)). - Fix documentation of the `POETRY_REPOSITORIES_` variables docs ([#8492](https://github.com/python-poetry/poetry/pull/8492)). - Add `CITATION.cff` file ([#8510](https://github.com/python-poetry/poetry/pull/8510)). ### poetry-core ([`1.8.1`](https://github.com/python-poetry/poetry-core/releases/tag/1.8.1)) - Add support for creating packages dynamically in the build script ([#629](https://github.com/python-poetry/poetry-core/pull/629)). - Improve marker logic for `extra` markers ([#636](https://github.com/python-poetry/poetry-core/pull/636)). - Update list of supported licenses ([#635](https://github.com/python-poetry/poetry-core/pull/635), [#646](https://github.com/python-poetry/poetry-core/pull/646)). - Fix an issue where projects with extension modules were not installed in editable mode ([#633](https://github.com/python-poetry/poetry-core/pull/633)). - Fix an issue where the wrong or no `lib` folder was added to the wheel ([#634](https://github.com/python-poetry/poetry-core/pull/634)). ### poetry-plugin-export ([`^1.6.0`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.6.0)) - Add an `--all-extras` option ([#241](https://github.com/python-poetry/poetry-plugin-export/pull/241)). - Fix an issue where git dependencies are exported with the branch name instead of the resolved commit hash ([#213](https://github.com/python-poetry/poetry-plugin-export/pull/213)). ## [1.6.1] - 2023-08-21 ### Fixed - Update the minimum required version of `requests` ([#8336](https://github.com/python-poetry/poetry/pull/8336)). ## [1.6.0] - 2023-08-20 ### Added - **Add support for repositories that do not provide a supported hash algorithm** ([#8118](https://github.com/python-poetry/poetry/pull/8118)). - **Add full support for duplicate dependencies with overlapping markers** ([#7257](https://github.com/python-poetry/poetry/pull/7257)). - **Improve performance of `poetry lock` for certain edge cases** ([#8256](https://github.com/python-poetry/poetry/pull/8256)). - Improve performance of `poetry install` ([#8031](https://github.com/python-poetry/poetry/pull/8031)). - `poetry check` validates that specified `readme` files do exist ([#7444](https://github.com/python-poetry/poetry/pull/7444)). - Add a downgrading note when updating to an older version ([#8176](https://github.com/python-poetry/poetry/pull/8176)). - Add support for `vox` in the `xonsh` shell ([#8203](https://github.com/python-poetry/poetry/pull/8203)). - Add support for `pre-commit` hooks for projects where the pyproject.toml file is located in a subfolder ([#8204](https://github.com/python-poetry/poetry/pull/8204)). - Add support for the `git+http://` scheme ([#6619](https://github.com/python-poetry/poetry/pull/6619)). ### Changed - **Drop support for Python 3.7** ([#7674](https://github.com/python-poetry/poetry/pull/7674)). - Move `poetry lock --check` to `poetry check --lock` and deprecate the former ([#8015](https://github.com/python-poetry/poetry/pull/8015)). - Change future warning that PyPI will only be disabled automatically if there are no primary sources ([#8151](https://github.com/python-poetry/poetry/pull/8151)). ### Fixed - Fix an issue where `build-system.requires` were not respected for projects with build scripts ([#7975](https://github.com/python-poetry/poetry/pull/7975)). - Fix an issue where the encoding was not handled correctly when calling a subprocess ([#8060](https://github.com/python-poetry/poetry/pull/8060)). - Fix an issue where `poetry show --top-level` did not show top level dependencies with extras ([#8076](https://github.com/python-poetry/poetry/pull/8076)). - Fix an issue where `poetry init` handled projects with `src` layout incorrectly ([#8218](https://github.com/python-poetry/poetry/pull/8218)). - Fix an issue where Poetry wrote `.pth` files with the wrong encoding ([#8041](https://github.com/python-poetry/poetry/pull/8041)). - Fix an issue where `poetry install` did not respect the source if the same version of a package has been locked from different sources ([#8304](https://github.com/python-poetry/poetry/pull/8304)). ### Docs - Document **official Poetry badge** ([#8066](https://github.com/python-poetry/poetry/pull/8066)). - Update configuration folder path for macOS ([#8062](https://github.com/python-poetry/poetry/pull/8062)). - Add a warning about pip ignoring lock files ([#8117](https://github.com/python-poetry/poetry/pull/8117)). - Clarify the use of the `virtualenvs.in-project` setting. ([#8126](https://github.com/python-poetry/poetry/pull/8126)). - Change `pre-commit` YAML style to be consistent with pre-commit's own examples ([#8146](https://github.com/python-poetry/poetry/pull/8146)). - Fix command for listing installed plugins ([#8200](https://github.com/python-poetry/poetry/pull/8200)). - Mention the `nox-poetry` package ([#8173](https://github.com/python-poetry/poetry/pull/8173)). - Add an example with a PyPI source in the pyproject.toml file ([#8171](https://github.com/python-poetry/poetry/pull/8171)). - Use `reference` instead of deprecated `callable` in the scripts example ([#8211](https://github.com/python-poetry/poetry/pull/8211)). ### poetry-core ([`1.7.0`](https://github.com/python-poetry/poetry-core/releases/tag/1.7.0)) - Improve performance of marker handling ([#609](https://github.com/python-poetry/poetry-core/pull/609)). - Allow `|` as a value separator in markers with the operators `in` and `not in` ([#608](https://github.com/python-poetry/poetry-core/pull/608)). - Put pretty name (instead of normalized name) in metadata ([#620](https://github.com/python-poetry/poetry-core/pull/620)). - Update list of supported licenses ([#623](https://github.com/python-poetry/poetry-core/pull/623)). - Fix an issue where PEP 508 dependency specifications with names starting with a digit could not be parsed ([#607](https://github.com/python-poetry/poetry-core/pull/607)). - Fix an issue where Poetry considered an unrelated `.gitignore` file resulting in an empty wheel ([#611](https://github.com/python-poetry/poetry-core/pull/611)). ### poetry-plugin-export ([`^1.5.0`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.5.0)) - Fix an issue where markers for dependencies required by an extra were not generated correctly ([#209](https://github.com/python-poetry/poetry-plugin-export/pull/209)). ## [1.5.1] - 2023-05-29 ### Added - Improve dependency resolution performance in cases with a lot of backtracking ([#7950](https://github.com/python-poetry/poetry/pull/7950)). ### Changed - Disable wheel content validation during installation ([#7987](https://github.com/python-poetry/poetry/pull/7987)). ### Fixed - Fix an issue where partially downloaded wheels were cached ([#7968](https://github.com/python-poetry/poetry/pull/7968)). - Fix an issue where `poetry run` did no longer execute relative-path scripts ([#7963](https://github.com/python-poetry/poetry/pull/7963)). - Fix an issue where dependencies were not installed in `in-project` environments ([#7977](https://github.com/python-poetry/poetry/pull/7977)). - Fix an issue where no solution was found for a transitive dependency on a pre-release of a package ([#7978](https://github.com/python-poetry/poetry/pull/7978)). - Fix an issue where cached repository packages were incorrectly parsed, leading to its dependencies being ignored ([#7995](https://github.com/python-poetry/poetry/pull/7995)). - Fix an issue where an explicit source was ignored so that a direct origin dependency was used instead ([#7973](https://github.com/python-poetry/poetry/pull/7973)). - Fix an issue where the installation of big wheels consumed a lot of memory ([#7987](https://github.com/python-poetry/poetry/pull/7987)). ### Docs - Add information about multiple constraints dependencies with direct origin and version dependencies ([#7973](https://github.com/python-poetry/poetry/pull/7973)). ### poetry-core ([`1.6.1`](https://github.com/python-poetry/poetry-core/releases/tag/1.6.1)) - Fix an endless recursion in marker handling ([#593](https://github.com/python-poetry/poetry-core/pull/593)). - Fix an issue where the wheel tag was not built correctly under certain circumstances ([#591](https://github.com/python-poetry/poetry-core/pull/591)). ### poetry-plugin-export ([`^1.4.0`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.4.0)) - Fix an issue where `--extra-index-url` and `--trusted-host` was not generated for sources with priority `explicit` ([#205](https://github.com/python-poetry/poetry-plugin-export/pull/205)). ## [1.5.0] - 2023-05-19 ### Added - **Introduce the new source priorities `explicit` and `supplemental`** ([#7658](https://github.com/python-poetry/poetry/pull/7658), [#6879](https://github.com/python-poetry/poetry/pull/6879)). - **Introduce the option to configure the priority of the implicit PyPI source** ([#7801](https://github.com/python-poetry/poetry/pull/7801)). - Add handling for corrupt cache files ([#7453](https://github.com/python-poetry/poetry/pull/7453)). - Improve caching of URL and git dependencies ([#7693](https://github.com/python-poetry/poetry/pull/7693), [#7473](https://github.com/python-poetry/poetry/pull/7473)). - Add option to skip installing directory dependencies ([#6845](https://github.com/python-poetry/poetry/pull/6845), [#7923](https://github.com/python-poetry/poetry/pull/7923)). - Add `--executable` option to `poetry env info` ([#7547](https://github.com/python-poetry/poetry/pull/7547)). - Add `--top-level` option to `poetry show` ([#7415](https://github.com/python-poetry/poetry/pull/7415)). - Add `--lock` option to `poetry remove` ([#7917](https://github.com/python-poetry/poetry/pull/7917)). - Add experimental `POETRY_REQUESTS_TIMEOUT` option ([#7081](https://github.com/python-poetry/poetry/pull/7081)). - Improve performance of wheel inspection by avoiding unnecessary file copy operations ([#7916](https://github.com/python-poetry/poetry/pull/7916)). ### Changed - **Remove the old deprecated installer and the corresponding setting `experimental.new-installer`** ([#7356](https://github.com/python-poetry/poetry/pull/7356)). - **Introduce `priority` key for sources and deprecate flags `default` and `secondary`** ([#7658](https://github.com/python-poetry/poetry/pull/7658)). - Deprecate `poetry run ` if the entry point was not previously installed via `poetry install` ([#7606](https://github.com/python-poetry/poetry/pull/7606)). - Only write the lock file if the installation succeeds ([#7498](https://github.com/python-poetry/poetry/pull/7498)). - Do not write the unused package category into the lock file ([#7637](https://github.com/python-poetry/poetry/pull/7637)). ### Fixed - Fix an issue where Poetry's internal pyproject.toml continually grows larger with empty lines ([#7705](https://github.com/python-poetry/poetry/pull/7705)). - Fix an issue where Poetry crashes due to corrupt cache files ([#7453](https://github.com/python-poetry/poetry/pull/7453)). - Fix an issue where the `Retry-After` in HTTP responses was not respected and retries were handled inconsistently ([#7072](https://github.com/python-poetry/poetry/pull/7072)). - Fix an issue where Poetry silently ignored invalid groups ([#7529](https://github.com/python-poetry/poetry/pull/7529)). - Fix an issue where Poetry does not find a compatible Python version if not given explicitly ([#7771](https://github.com/python-poetry/poetry/pull/7771)). - Fix an issue where the `direct_url.json` of an editable install from a git dependency was invalid ([#7473](https://github.com/python-poetry/poetry/pull/7473)). - Fix an issue where error messages from build backends were not decoded correctly ([#7781](https://github.com/python-poetry/poetry/pull/7781)). - Fix an infinite loop when adding certain dependencies ([#7405](https://github.com/python-poetry/poetry/pull/7405)). - Fix an issue where pre-commit hooks skip pyproject.toml files in subdirectories ([#7239](https://github.com/python-poetry/poetry/pull/7239)). - Fix an issue where pre-commit hooks do not use the expected Python version ([#6989](https://github.com/python-poetry/poetry/pull/6989)). - Fix an issue where an unclear error message is printed if the project name is the same as one of its dependencies ([#7757](https://github.com/python-poetry/poetry/pull/7757)). - Fix an issue where `poetry install` returns a zero exit status even though the build script failed ([#7812](https://github.com/python-poetry/poetry/pull/7812)). - Fix an issue where an existing `.venv` was not used if `in-project` was not set ([#7792](https://github.com/python-poetry/poetry/pull/7792)). - Fix an issue where multiple extras passed to `poetry add` were not parsed correctly ([#7836](https://github.com/python-poetry/poetry/pull/7836)). - Fix an issue where `poetry shell` did not send a newline to `fish` ([#7884](https://github.com/python-poetry/poetry/pull/7884)). - Fix an issue where `poetry update --lock` printed operations that were not executed ([#7915](https://github.com/python-poetry/poetry/pull/7915)). - Fix an issue where `poetry add --lock` did perform a full update of all dependencies ([#7920](https://github.com/python-poetry/poetry/pull/7920)). - Fix an issue where `poetry shell` did not work with `nushell` ([#7919](https://github.com/python-poetry/poetry/pull/7919)). - Fix an issue where subprocess calls failed on Python 3.7 ([#7932](https://github.com/python-poetry/poetry/pull/7932)). - Fix an issue where keyring was called even though the password was stored in an environment variable ([#7928](https://github.com/python-poetry/poetry/pull/7928)). ### Docs - Add information about what to use instead of `--dev` ([#7647](https://github.com/python-poetry/poetry/pull/7647)). - Promote semantic versioning less aggressively ([#7517](https://github.com/python-poetry/poetry/pull/7517)). - Explain Poetry's own versioning scheme in the FAQ ([#7517](https://github.com/python-poetry/poetry/pull/7517)). - Update documentation for configuration with environment variables ([#6711](https://github.com/python-poetry/poetry/pull/6711)). - Add details how to disable the virtualenv prompt ([#7874](https://github.com/python-poetry/poetry/pull/7874)). - Improve documentation on whether to commit `poetry.lock` ([#7506](https://github.com/python-poetry/poetry/pull/7506)). - Improve documentation of `virtualenv.create` ([#7608](https://github.com/python-poetry/poetry/pull/7608)). ### poetry-core ([`1.6.0`](https://github.com/python-poetry/poetry-core/releases/tag/1.6.0)) - Improve error message for invalid markers ([#569](https://github.com/python-poetry/poetry-core/pull/569)). - Increase robustness when deleting temporary directories on Windows ([#460](https://github.com/python-poetry/poetry-core/pull/460)). - Replace `tomlkit` with `tomli`, which changes the interface of some _internal_ classes ([#483](https://github.com/python-poetry/poetry-core/pull/483)). - Deprecate `Package.category` ([#561](https://github.com/python-poetry/poetry-core/pull/561)). - Fix a performance regression in marker handling ([#568](https://github.com/python-poetry/poetry-core/pull/568)). - Fix an issue where wildcard version constraints were not handled correctly ([#402](https://github.com/python-poetry/poetry-core/pull/402)). - Fix an issue where `poetry build` created duplicate Python classifiers if they were specified manually ([#578](https://github.com/python-poetry/poetry-core/pull/578)). - Fix an issue where local versions where not handled correctly ([#579](https://github.com/python-poetry/poetry-core/pull/579)). ## [1.4.2] - 2023-04-02 ### Changed - When trying to install wheels with invalid `RECORD` files, Poetry does not fail anymore but only prints a warning. This mitigates an unintended change introduced in Poetry 1.4.1 ([#7694](https://github.com/python-poetry/poetry/pull/7694)). ### Fixed - Fix an issue where relative git submodule urls were not parsed correctly ([#7017](https://github.com/python-poetry/poetry/pull/7017)). - Fix an issue where Poetry could freeze when building a project with a build script if it generated enough output to fill the OS pipe buffer ([#7699](https://github.com/python-poetry/poetry/pull/7699)). ## [1.4.1] - 2023-03-19 ### Fixed - Fix an issue where `poetry install` did not respect the requirements for building editable dependencies ([#7579](https://github.com/python-poetry/poetry/pull/7579)). - Fix an issue where `poetry init` crashed due to bad input when adding packages interactively ([#7569](https://github.com/python-poetry/poetry/pull/7569)). - Fix an issue where `poetry install` ignored the `subdirectory` argument of git dependencies ([#7580](https://github.com/python-poetry/poetry/pull/7580)). - Fix an issue where installing packages with `no-binary` could result in a false hash mismatch ([#7594](https://github.com/python-poetry/poetry/pull/7594)). - Fix an issue where the hash of sdists was neither validated nor written to the `direct_url.json` during installation ([#7594](https://github.com/python-poetry/poetry/pull/7594)). - Fix an issue where `poetry install --sync` attempted to remove itself ([#7626](https://github.com/python-poetry/poetry/pull/7626)). - Fix an issue where wheels with non-normalized `dist-info` directory names could not be installed ([#7671](https://github.com/python-poetry/poetry/pull/7671)). - Fix an issue where `poetry install --compile` compiled with optimization level 1 ([#7666](https://github.com/python-poetry/poetry/pull/7666)). ### Docs - Clarify the behavior of the `--extras` option ([#7563](https://github.com/python-poetry/poetry/pull/7563)). - Expand the FAQ on reasons for slow dependency resolution ([#7620](https://github.com/python-poetry/poetry/pull/7620)). ### poetry-core ([`1.5.2`](https://github.com/python-poetry/poetry-core/releases/tag/1.5.2)) - Fix an issue where wheels built on Windows could contain duplicate entries in the RECORD file ([#555](https://github.com/python-poetry/poetry-core/pull/555)). ## [1.4.0] - 2023-02-27 ### Added - **Add a modern installer (`installer.modern-installation`) for faster installation of packages and independence from pip** ([#6205](https://github.com/python-poetry/poetry/pull/6205)). - Add support for `Private ::` trove classifiers ([#7271](https://github.com/python-poetry/poetry/pull/7271)). - Add the version of poetry in the `@generated` comment at the beginning of the lock file ([#7339](https://github.com/python-poetry/poetry/pull/7339)). - Add support for `virtualenvs.prefer-active-python` when running `poetry new` and `poetry init` ([#7100](https://github.com/python-poetry/poetry/pull/7100)). ### Changed - **Deprecate the old installer, i.e. setting `experimental.new-installer` to `false`** ([#7358](https://github.com/python-poetry/poetry/pull/7358)). - Remove unused `platform` field from cached package info and bump the cache version ([#7304](https://github.com/python-poetry/poetry/pull/7304)). - Extra dependencies of the root project are now sorted in the lock file ([#7375](https://github.com/python-poetry/poetry/pull/7375)). - Remove upper boundary for `importlib-metadata` dependency ([#7434](https://github.com/python-poetry/poetry/pull/7434)). - Validate path dependencies during use instead of during construction ([#6844](https://github.com/python-poetry/poetry/pull/6844)). - Remove the deprecated `repository` modules ([#7468](https://github.com/python-poetry/poetry/pull/7468)). ### Fixed - Fix an issue where an unconditional dependency of an extra was not installed in specific environments ([#7175](https://github.com/python-poetry/poetry/pull/7175)). - Fix an issue where a pre-release of a dependency was chosen even if a stable release fulfilled the constraint ([#7225](https://github.com/python-poetry/poetry/pull/7225), [#7236](https://github.com/python-poetry/poetry/pull/7236)). - Fix an issue where HTTP redirects were not handled correctly during publishing ([#7160](https://github.com/python-poetry/poetry/pull/7160)). - Fix an issue where `poetry check` did not handle the `-C, --directory` option correctly ([#7241](https://github.com/python-poetry/poetry/pull/7241)). - Fix an issue where the subdirectory information of a git dependency was not written to the lock file ([#7367](https://github.com/python-poetry/poetry/pull/7367)). - Fix an issue where the wrong Python version was selected when creating an virtual environment ([#7221](https://github.com/python-poetry/poetry/pull/7221)). - Fix an issue where packages that should be kept were uninstalled when calling `poetry install --sync` ([#7389](https://github.com/python-poetry/poetry/pull/7389)). - Fix an issue where an incorrect value was set for `sys.argv[0]` when running installed scripts ([#6737](https://github.com/python-poetry/poetry/pull/6737)). - Fix an issue where hashes in `direct_url.json` files were not written according to the specification ([#7475](https://github.com/python-poetry/poetry/pull/7475)). - Fix an issue where poetry commands failed due to special characters in the path of the project or virtual environment ([#7471](https://github.com/python-poetry/poetry/pull/7471)). - Fix an issue where poetry crashed with a `JSONDecodeError` when running a Python script that produced certain warnings ([#6665](https://github.com/python-poetry/poetry/pull/6665)). ### Docs - Add advice on how to maintain a poetry plugin ([#6977](https://github.com/python-poetry/poetry/pull/6977)). - Update tox examples to comply with the latest tox release ([#7341](https://github.com/python-poetry/poetry/pull/7341)). - Mention that the `poetry export` can export `constraints.txt` files ([#7383](https://github.com/python-poetry/poetry/pull/7383)). - Add clarifications for moving configuration files ([#6864](https://github.com/python-poetry/poetry/pull/6864)). - Mention the different types of exact version specifications ([#7503](https://github.com/python-poetry/poetry/pull/7503)). ### poetry-core ([`1.5.1`](https://github.com/python-poetry/poetry-core/releases/tag/1.5.1)) - Improve marker handling ([#528](https://github.com/python-poetry/poetry-core/pull/528), [#534](https://github.com/python-poetry/poetry-core/pull/534), [#530](https://github.com/python-poetry/poetry-core/pull/530), [#546](https://github.com/python-poetry/poetry-core/pull/546), [#547](https://github.com/python-poetry/poetry-core/pull/547)). - Validate whether dependencies referenced in `extras` are defined in the main dependency group ([#542](https://github.com/python-poetry/poetry-core/pull/542)). - Poetry no longer generates a `setup.py` file in sdists by default ([#318](https://github.com/python-poetry/poetry-core/pull/318)). - Fix an issue where trailing newlines were allowed in `tool.poetry.description` ([#505](https://github.com/python-poetry/poetry-core/pull/505)). - Fix an issue where the name of the data folder in wheels was not normalized ([#532](https://github.com/python-poetry/poetry-core/pull/532)). - Fix an issue where the order of entries in the RECORD file was not deterministic ([#545](https://github.com/python-poetry/poetry-core/pull/545)). - Fix an issue where zero padding was not correctly handled in version comparisons ([#540](https://github.com/python-poetry/poetry-core/pull/540)). - Fix an issue where sdist builds did not support multiple READMEs ([#486](https://github.com/python-poetry/poetry-core/pull/486)). ### poetry-plugin-export ([`^1.3.0`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.3.0)) - Fix an issue where the export failed if there was a circular dependency on the root package ([#118](https://github.com/python-poetry/poetry-plugin-export/pull/118)). ## [1.3.2] - 2023-01-10 ### Fixed - Fix a performance regression when locking dependencies from PyPI ([#7232](https://github.com/python-poetry/poetry/pull/7232)). - Fix an issue where passing a relative path via `-C, --directory` fails ([#7266](https://github.com/python-poetry/poetry/pull/7266)). ### Docs - Update docs to reflect the removal of the deprecated `get-poetry.py` installer from the repository ([#7288](https://github.com/python-poetry/poetry/pull/7288)). - Add clarifications for `virtualenvs.path` settings ([#7286](https://github.com/python-poetry/poetry/pull/7286)). ## [1.3.1] - 2022-12-12 ### Fixed - Fix an issue where an explicit dependency on `lockfile` was missing, resulting in a broken Poetry in rare circumstances ([7169](https://github.com/python-poetry/poetry/pull/7169)). ## [1.3.0] - 2022-12-09 ### Added - Mark the lock file with an `@generated` comment as used by common tooling ([#2773](https://github.com/python-poetry/poetry/pull/2773)). - `poetry check` validates trove classifiers and warns for deprecations ([#2881](https://github.com/python-poetry/poetry/pull/2881)). - Introduce a top level `-C, --directory` option to set the working path ([#6810](https://github.com/python-poetry/poetry/pull/6810)). ### Changed - **New lock file format (version 2.0)** ([#6393](https://github.com/python-poetry/poetry/pull/6393)). - Path dependency metadata is unconditionally re-locked ([#6843](https://github.com/python-poetry/poetry/pull/6843)). - URL dependency hashes are locked ([#7121](https://github.com/python-poetry/poetry/pull/7121)). - `poetry update` and `poetry lock` should now resolve dependencies more similarly ([#6477](https://github.com/python-poetry/poetry/pull/6477)). - `poetry publish` will report more useful errors when a file does not exist ([#4417](https://github.com/python-poetry/poetry/pull/4417)). - `poetry add` will check for duplicate entries using canonical names ([#6832](https://github.com/python-poetry/poetry/pull/6832)). - Wheels are preferred to source distributions when gathering metadata ([#6547](https://github.com/python-poetry/poetry/pull/6547)). - Git dependencies of extras are only fetched if the extra is requested ([#6615](https://github.com/python-poetry/poetry/pull/6615)). - Invoke `pip` with `--no-input` to prevent hanging without feedback ([#6724](https://github.com/python-poetry/poetry/pull/6724), [#6966](https://github.com/python-poetry/poetry/pull/6966)). - Invoke `pip` with `--isolated` to prevent the influence of user configuration ([#6531](https://github.com/python-poetry/poetry/pull/6531)). - Interrogate environments with Python in isolated (`-I`) mode ([#6628](https://github.com/python-poetry/poetry/pull/6628)). - Raise an informative error when multiple version constraints overlap and are incompatible ([#7098](https://github.com/python-poetry/poetry/pull/7098)). ### Fixed - **Fix an issue where concurrent instances of Poetry would corrupt the artifact cache** ([#6186](https://github.com/python-poetry/poetry/pull/6186)). - **Fix an issue where Poetry can hang after being interrupted due to stale locking in cache** ([#6471](https://github.com/python-poetry/poetry/pull/6471)). - Fix an issue where the output of commands executed with `--dry-run` contained duplicate entries ([#4660](https://github.com/python-poetry/poetry/pull/4660)). - Fix an issue where `requests`'s pool size did not match the number of installer workers ([#6805](https://github.com/python-poetry/poetry/pull/6805)). - Fix an issue where `poetry show --outdated` failed with a runtime error related to direct origin dependencies ([#6016](https://github.com/python-poetry/poetry/pull/6016)). - Fix an issue where only the last command of an `ApplicationPlugin` is registered ([#6304](https://github.com/python-poetry/poetry/pull/6304)). - Fix an issue where git dependencies were fetched unnecessarily when running `poetry lock --no-update` ([#6131](https://github.com/python-poetry/poetry/pull/6131)). - Fix an issue where stdout was polluted with messages that should go to stderr ([#6429](https://github.com/python-poetry/poetry/pull/6429)). - Fix an issue with `poetry shell` activation and zsh ([#5795](https://github.com/python-poetry/poetry/pull/5795)). - Fix an issue where a url dependencies were shown as outdated ([#6396](https://github.com/python-poetry/poetry/pull/6396)). - Fix an issue where the `source` field of a dependency with extras was ignored ([#6472](https://github.com/python-poetry/poetry/pull/6472)). - Fix an issue where a package from the wrong source was installed for a multiple-constraints dependency with different sources ([#6747](https://github.com/python-poetry/poetry/pull/6747)). - Fix an issue where dependencies from different sources where merged during dependency resolution ([#6679](https://github.com/python-poetry/poetry/pull/6679)). - Fix an issue where `experimental.system-git-client` could not be used via environment variable ([#6783](https://github.com/python-poetry/poetry/pull/6783)). - Fix an issue where Poetry fails with an `AssertionError` due to `distribution.files` being `None` ([#6788](https://github.com/python-poetry/poetry/pull/6788)). - Fix an issue where `poetry env info` did not respect `virtualenvs.prefer-active-python` ([#6986](https://github.com/python-poetry/poetry/pull/6986)). - Fix an issue where `poetry env list` does not list the in-project environment ([#6979](https://github.com/python-poetry/poetry/pull/6979)). - Fix an issue where `poetry env remove` removed the wrong environment ([#6195](https://github.com/python-poetry/poetry/pull/6195)). - Fix an issue where the return code of a script was not relayed as exit code ([#6824](https://github.com/python-poetry/poetry/pull/6824)). - Fix an issue where the solver could silently swallow `ValueError` ([#6790](https://github.com/python-poetry/poetry/pull/6790)). ### Docs - Improve documentation of package sources ([#5605](https://github.com/python-poetry/poetry/pull/5605)). - Correct the default cache path on Windows ([#7012](https://github.com/python-poetry/poetry/pull/7012)). ### poetry-core ([`1.4.0`](https://github.com/python-poetry/poetry-core/releases/tag/1.4.0)) - The PEP 517 `metadata_directory` is now respected as an input to the `build_wheel` hook ([#487](https://github.com/python-poetry/poetry-core/pull/487)). - `ParseConstraintError` is now raised on version and constraint parsing errors, and includes information on the package that caused the error ([#514](https://github.com/python-poetry/poetry-core/pull/514)). - Fix an issue where invalid PEP 508 requirements were generated due to a missing space before semicolons ([#510](https://github.com/python-poetry/poetry-core/pull/510)). - Fix an issue where relative paths were encoded into package requirements, instead of a file:// URL as required by PEP 508 ([#512](https://github.com/python-poetry/poetry-core/pull/512)). ### poetry-plugin-export ([`^1.2.0`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.2.0)) - Ensure compatibility with Poetry 1.3.0. No functional changes. ### cleo ([`^2.0.0`](https://github.com/python-poetry/poetry-core/releases/tag/2.0.0)) - Fix an issue where shell completions had syntax errors ([#247](https://github.com/python-poetry/cleo/pull/247)). - Fix an issue where not reading all the output of a command resulted in a "Broken pipe" error ([#165](https://github.com/python-poetry/cleo/pull/165)). - Fix an issue where errors were not shown in non-verbose mode ([#166](https://github.com/python-poetry/cleo/pull/166)). ## [1.2.2] - 2022-10-10 ### Added - Add forward compatibility for lock file format 2.0, which will be used by Poetry 1.3 ([#6608](https://github.com/python-poetry/poetry/pull/6608)). ### Changed - Allow `poetry lock` to re-generate the lock file when invalid or incompatible ([#6753](https://github.com/python-poetry/poetry/pull/6753)). ### Fixed - Fix an issue where the deprecated JSON API was used to query PyPI for available versions of a package ([#6081](https://github.com/python-poetry/poetry/pull/6081)). - Fix an issue where versions were escaped wrongly when building the wheel name ([#6476](https://github.com/python-poetry/poetry/pull/6476)). - Fix an issue where the installation of dependencies failed if pip is a dependency and is updated in parallel to other dependencies ([#6582](https://github.com/python-poetry/poetry/pull/6582)). - Fix an issue where the names of extras were not normalized according to PEP 685 ([#6541](https://github.com/python-poetry/poetry/pull/6541)). - Fix an issue where sdist names were not normalized ([#6621](https://github.com/python-poetry/poetry/pull/6621)). - Fix an issue where invalid constraints, which are ignored, were only reported in a debug message instead of a warning ([#6730](https://github.com/python-poetry/poetry/pull/6730)). - Fix an issue where `poetry shell` was broken in git bash on Windows ([#6560](https://github.com/python-poetry/poetry/pull/6560)). ### Docs - Rework the README and contribution docs ([#6552](https://github.com/python-poetry/poetry/pull/6552)). - Fix for inconsistent docs for multiple-constraint dependencies ([#6604](https://github.com/python-poetry/poetry/pull/6604)). - Rephrase plugin configuration ([#6557](https://github.com/python-poetry/poetry/pull/6557)). - Add a note about publishable repositories to `publish` ([#6641](https://github.com/python-poetry/poetry/pull/6641)). - Fix the path for lazy-loaded bash completion ([#6656](https://github.com/python-poetry/poetry/pull/6656)). - Fix a reference to the invalid option `--require` ([#6672](https://github.com/python-poetry/poetry/pull/6672)). - Add a PowerShell one-liner to the basic usage section ([#6683](https://github.com/python-poetry/poetry/pull/6683)). - Fix the minimum poetry version in the example for plugins ([#6739](https://github.com/python-poetry/poetry/pull/6739)). ### poetry-core ([`1.3.2`](https://github.com/python-poetry/poetry-core/releases/tag/1.3.2)) - Add `3.11` to the list of available Python versions ([#477](https://github.com/python-poetry/poetry-core/pull/477)). - Fix an issue where caret constraints of pre-releases with a major version of 0 resulted in an empty version range ([#475](https://github.com/python-poetry/poetry-core/pull/475)). ### poetry-plugin-export ([`^1.1.2`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.1.2)) - Add support for exporting `constraints.txt` files ([#128](https://github.com/python-poetry/poetry-plugin-export/pull/128)). - Fix an issue where a relative path passed via `-o` was not interpreted relative to the current working directory ([#130](https://github.com/python-poetry/poetry-plugin-export/pull/130)). ## [1.2.1] - 2022-09-16 ### Changed - Bump `poetry-core` to [`1.2.0`](https://github.com/python-poetry/poetry-core/releases/tag/1.2.0). - Bump `poetry-plugin-export` to [`^1.0.7`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.0.7). ### Fixed - Fix an issue where `poetry cache clear` did not respect the `-n/--no-interaction` flag ([#6338](https://github.com/python-poetry/poetry/pull/6338)). - Fix an issue where `poetry lock --no-update` updated dependencies from non-PyPI package sources ([#6335](https://github.com/python-poetry/poetry/pull/6335)). - Fix a `poetry install` performance regression by falling back to internal pip ([#6062](https://github.com/python-poetry/poetry/pull/6062)). - Fix an issue where a virtual environment was created unnecessarily when running `poetry export` ([#6282](https://github.com/python-poetry/poetry/pull/6282)). - Fix an issue where `poetry lock --no-update` added duplicate hashes to the lock file ([#6389](https://github.com/python-poetry/poetry/pull/6389)). - Fix an issue where `poetry install` fails because of missing hashes for `url` dependencies ([#6389](https://github.com/python-poetry/poetry/pull/6389)). - Fix an issue where Poetry was not able to update pip in Windows virtual environments ([#6430](https://github.com/python-poetry/poetry/pull/6430)). - Fix an issue where Poetry was not able to install releases that contained less common link types ([#5767](https://github.com/python-poetry/poetry/pull/5767)). - Fix a `poetry lock` performance regression when checking non-PyPI sources for yanked versions ([#6442](https://github.com/python-poetry/poetry/pull/6442)). - Fix an issue where `--no-cache` was not respected when running `poetry install` ([#6479](https://github.com/python-poetry/poetry/pull/6479)). - Fix an issue where deprecation warnings for `--dev` were missing ([#6475](https://github.com/python-poetry/poetry/pull/6475)). - Fix an issue where Git dependencies failed to clone when `insteadOf` was used in `.gitconfig` using the Dulwich Git client ([#6506](https://github.com/python-poetry/poetry/pull/6506)). - Fix an issue where no cache entry is found when calling `poetry cache clear` with a non-normalized package name ([#6537](https://github.com/python-poetry/poetry/pull/6537)). - Fix an invalid virtualenv constraint on Poetry ([#6402](https://github.com/python-poetry/poetry/pull/6402)). - Fix outdated build system requirements for Poetry ([#6509](https://github.com/python-poetry/poetry/pull/6509)). ### Docs - Add missing path segment to paths used by install.python-poetry.org ([#6311](https://github.com/python-poetry/poetry/pull/6311)). - Add recommendations about how to install Poetry in a CI environment ([#6345](https://github.com/python-poetry/poetry/pull/6345)). - Fix examples for `--with` and `--without` ([#6318](https://github.com/python-poetry/poetry/pull/6318)). - Update configuration folder path for macOS ([#6395](https://github.com/python-poetry/poetry/pull/6395)). - Improve the description of the `virtualenv.create` option ([#6460](https://github.com/python-poetry/poetry/pull/6460)). - Clarify that `poetry install` removes dependencies of non-installed extras ([#6229](https://github.com/python-poetry/poetry/pull/6229)). - Add a note about `pre-commit autoupdate` and Poetry's hooks ([#6497](https://github.com/python-poetry/poetry/pull/6497)). ## [1.2.0] - 2022-08-31 ### Docs - Added note about how to add a git dependency with a subdirectory ([#6218](https://github.com/python-poetry/poetry/pull/6218)) - Fixed several style issues in the docs ([#6254](https://github.com/python-poetry/poetry/pull/6254)) - Fixed outdated info about `--only` parameter ([#6263](https://github.com/python-poetry/poetry/pull/6263)) ## [1.2.0rc2] - 2022-08-26 ### Fixed - Fixed an issue where virtual environments were created unnecessarily when running `poetry self` commands ([#6225](https://github.com/python-poetry/poetry/pull/6225)) - Ensure that packages' `pretty_name` are written to the lock file ([#6237](https://github.com/python-poetry/poetry/pull/6237)) ### Improvements - Improved the consistency of `Pool().remove_repository()` to make it easier to write poetry plugins ([#6214](https://github.com/python-poetry/poetry/pull/6214)) ### Docs - Removed mentions of Python 2.7 from docs ([#6234](https://github.com/python-poetry/poetry/pull/6234)) - Added note about the difference between groups and extras ([#6230](https://github.com/python-poetry/poetry/pull/6230)) ## [1.2.0rc1] - 2022-08-22 ### Added - Added support for subdirectories in git dependencies ([#5172](https://github.com/python-poetry/poetry/pull/5172)) - Added support for yanked releases and files (PEP-592) ([#5841](https://github.com/python-poetry/poetry/pull/5841)) - Virtual environments can now be created even with empty project names ([#5856](https://github.com/python-poetry/poetry/pull/5856)) - Added support for `nushell` in `poetry shell` ([#6063](https://github.com/python-poetry/poetry/pull/6063)) ### Changed - Poetry now falls back to gather metadata for dependencies via pep517 if parsing `pyproject.toml` fails ([#5834](https://github.com/python-poetry/poetry/pull/5834)) - Replaced Poetry's helper method `canonicalize_name()` with `packaging.utils.canonicalize_name()` ([#6022](https://github.com/python-poetry/poetry/pull/6022)) - Removed code for the `export` command, which is now provided via plugin ([#6128](https://github.com/python-poetry/poetry/pull/6128)) - Extras and extras dependencies are now sorted in the lock file ([#6169](https://github.com/python-poetry/poetry/pull/6169)) - Removed deprecated (1.2-only) CLI options ([#6210](https://github.com/python-poetry/poetry/pull/6210)) ### Fixed - Fixed an issue where symlinks in the lock file were not resolved ([#5850](https://github.com/python-poetry/poetry/pull/5850)) - Fixed a `tomlkit` regression resulting in inconsistent line endings ([#5870](https://github.com/python-poetry/poetry/pull/5870)) - Fixed an issue where the `POETRY_PYPI_TOKEN_PYPI` environment variable wasn't respected ([#5911](https://github.com/python-poetry/poetry/pull/5911)) - Fixed an issue where neither Python nor a managed venv can be found, when using Python from MS Store ([#5931](https://github.com/python-poetry/poetry/pull/5931)) - Improved error message of `poetry publish` in the event of an upload error ([#6043](https://github.com/python-poetry/poetry/pull/6043)) - Fixed an issue where `poetry lock` fails without output ([#6058](https://github.com/python-poetry/poetry/pull/6058)) - Fixed an issue where Windows drive mappings break virtual environment names ([#6110](https://github.com/python-poetry/poetry/pull/6110)) - `tomlkit` versions with memory leak are now avoided ([#6160](https://github.com/python-poetry/poetry/pull/6160)) - Fixed an infinite loop in the solver ([#6178](https://github.com/python-poetry/poetry/pull/6178)) - Fixed an issue where latest version was used instead of locked one for vcs dependencies with extras ([#6185](https://github.com/python-poetry/poetry/pull/6185)) ### Docs - Document use of the `subdirectory` parameter ([#5949](https://github.com/python-poetry/poetry/pull/5949)) - Document suggested `tox` config for different use cases ([#6026](https://github.com/python-poetry/poetry/pull/6026)) ## [1.1.15] - 2022-08-22 ### Changed - Poetry now fallback to gather metadata for dependencies via pep517 if parsing pyproject.toml fail ([#6206](https://github.com/python-poetry/poetry/pull/6206)) - Extras and extras dependencies are now sorted in lock file ([#6207](https://github.com/python-poetry/poetry/pull/6207)) ## [1.2.0b3] - 2022-07-13 **Important**: This release fixes a critical issue that prevented hashes from being retrieved when locking dependencies, due to a breaking change on PyPI JSON API (see [#5972](https://github.com/python-poetry/poetry/pull/5972) and [the upstream change](https://github.com/pypi/warehouse/pull/11775) for more details). After upgrading, you have to clear Poetry cache manually to get that feature working correctly again: ```bash $ poetry cache clear pypi --all ``` ### Added - Added `--only-root` to `poetry install` to install a project without its dependencies ([#5783](https://github.com/python-poetry/poetry/pull/5783)) ### Changed - Improved user experience of `poetry init` ([#5838](https://github.com/python-poetry/poetry/pull/5838)) - Added default timeout for all HTTP requests, to avoid hanging requests ([#5881](https://github.com/python-poetry/poetry/pull/5881)) - Updated `poetry init` to better specify how to skip adding dependencies ([#5946](https://github.com/python-poetry/poetry/pull/5946)) - Updated Poetry repository names to avoid clashes with user-defined repositories ([#5910](https://github.com/python-poetry/poetry/pull/5910)) ### Fixed - Fixed an issue where extras where not handled if they did not match the case-sensitive name of the packages ([#4122](https://github.com/python-poetry/poetry/pull/4122)) - Fixed configuration of `experimental.system-git-client` option through `poetry config` ([#5818](https://github.com/python-poetry/poetry/pull/5818)) - Fixed uninstallation of git dependencies on Windows ([#5836](https://github.com/python-poetry/poetry/pull/5836)) - Fixed an issue where `~` was not correctly expanded in `virtualenvs.path` ([#5848](https://github.com/python-poetry/poetry/pull/5848)) - Fixed an issue where installing/locking dependencies would hang when setting an incorrect git repository ([#5880](https://github.com/python-poetry/poetry/pull/5880)) - Fixed an issue in `poetry publish` when keyring was not properly configured ([#5889](https://github.com/python-poetry/poetry/pull/5889)) - Fixed duplicated line output in console ([#5890](https://github.com/python-poetry/poetry/pull/5890)) - Fixed an issue where the same wheels where downloaded multiple times during installation ([#5871](https://github.com/python-poetry/poetry/pull/5871)) - Fixed an issue where dependencies hashes could not be retrieved when locking due to a breaking change on PyPI JSON API ([#5973](https://github.com/python-poetry/poetry/pull/5973)) - Fixed an issue where a dependency with non-requested extras could not be installed if it is requested with extras by another dependency ([#5770](https://github.com/python-poetry/poetry/pull/5770)) - Updated git backend to correctly read local/global git config when using dulwich as a git backend ([#5935](https://github.com/python-poetry/poetry/pull/5935)) - Fixed an issue where optional dependencies where not correctly exported when defining groups ([#5819](https://github.com/python-poetry/poetry/pull/5819)) ### Docs - Fixed configuration instructions for repositories specification ([#5809](https://github.com/python-poetry/poetry/pull/5809)) - Added a link to dependency specification from `pyproject.toml` ([#5815](https://github.com/python-poetry/poetry/pull/5815)) - Improved `zsh` autocompletion instructions ([#5859](https://github.com/python-poetry/poetry/pull/5859)) - Improved installation and update documentations ([#5857](https://github.com/python-poetry/poetry/pull/5857)) - Improved exact requirements documentation ([#5874](https://github.com/python-poetry/poetry/pull/5874)) - Added documentation for `@` operator ([#5822](https://github.com/python-poetry/poetry/pull/5822)) - Improved autocompletion documentation ([#5879](https://github.com/python-poetry/poetry/pull/5879)) - Improved `scripts` definition documentation ([#5884](https://github.com/python-poetry/poetry/pull/5884)) ## [1.1.14] - 2022-07-08 ### Fixed - Fixed an issue where dependencies hashes could not be retrieved when locking due to a breaking change on PyPI JSON API ([#5973](https://github.com/python-poetry/poetry/pull/5973)) ## [1.2.0b2] - 2022-06-07 ### Added - Added support for multiple-constraint direct origin dependencies with the same version ([#5715](https://github.com/python-poetry/poetry/pull/5715)) - Added support disabling TLS verification for custom package sources via `poetry config certificates..cert false` ([#5719](https://github.com/python-poetry/poetry/pull/5719) - Added new configuration (`virtualenvs.prompt`) to customize the prompt of the Poetry-managed virtual environment ([#5606](https://github.com/python-poetry/poetry/pull/5606)) - Added progress indicator to `download_file` (used when downloading dists) ([#5451](https://github.com/python-poetry/poetry/pull/5451)) - Added `--dry-run` to `poetry version` command ([#5603](https://github.com/python-poetry/poetry/pull/5603)) - Added `--why` to `poetry show` ([#5444](https://github.com/python-poetry/poetry/pull/5444)) - Added support for single page (html) repositories ([#5517](https://github.com/python-poetry/poetry/pull/5517)) - Added support for PEP 508 strings when adding dependencies via `poetry add` command ([#5554](https://github.com/python-poetry/poetry/pull/5554)) - Added `--no-cache` as a global option ([#5519](https://github.com/python-poetry/poetry/pull/5519)) - Added cert retrieval for HTTP requests made by Poetry ([#5320](https://github.com/python-poetry/poetry/pull/5320)) - Added `--skip-existing` to `poetry publish` ([#2812](https://github.com/python-poetry/poetry/pull/2812)) - Added `--all-extras` to `poetry install` ([#5452](https://github.com/python-poetry/poetry/pull/5452)) - Added new `poetry self` sub-commands to manage plugins and/or system environment packages, eg: keyring backends ([#5450](https://github.com/python-poetry/poetry/pull/5450)) - Added new configuration (`installer.no-binary`) to allow selection of non-binary distributions when installing a dependency ([#5609](https://github.com/python-poetry/poetry/pull/5609)) ### Changed - `poetry plugin` commands are now deprecated in favor of the more generic `poetry self` commands ([#5450](https://github.com/python-poetry/poetry/pull/5450)) - When creating new projects, Poetry no longer restricts README extensions to `md` and `rst` ([#5357](https://github.com/python-poetry/poetry/pull/5357)) - Changed the provider to allow fallback to installed packages ([#5704](https://github.com/python-poetry/poetry/pull/5704)) - Solver now correctly handles and prefers direct reference constraints (vcs, file etc.) over public version identifiers ([#5654](https://github.com/python-poetry/poetry/pull/5654)) - Changed the build script behavior to create an ephemeral build environment when a build script is specified ([#5401](https://github.com/python-poetry/poetry/pull/5401)) - Improved performance when determining PEP 517 metadata from sources ([#5601](https://github.com/python-poetry/poetry/pull/5601)) - Project package sources no longer need to be redefined as global repositories when configuring credentials ([#5563](https://github.com/python-poetry/poetry/pull/5563)) - Replaced external git command use with dulwich, in order to force the legacy behaviour set `experimental.system-git-client` configuration to `true` ([#5428](https://github.com/python-poetry/poetry/pull/5428)) - Improved http request handling for sources and multiple paths on same netloc ([#5518](https://github.com/python-poetry/poetry/pull/5518)) - Made `no-pip` and `no-setuptools` configuration explicit ([#5455](https://github.com/python-poetry/poetry/pull/5455)) - Improved application logging, use of `-vv` now provides more debug information ([#5503](https://github.com/python-poetry/poetry/pull/5503)) - Renamed implicit group `default` to `main` ([#5465](https://github.com/python-poetry/poetry/pull/5465)) - Replaced in-tree implementation of `poetry export` with `poetry-plugin-export` ([#5413](https://github.com/python-poetry/poetry/pull/5413)) - Changed the password manager behavior to use a `"null"` keyring when disabled ([#5251](https://github.com/python-poetry/poetry/pull/5251)) - Incremental improvement of Solver performance ([#5335](https://github.com/python-poetry/poetry/pull/5335)) - Newly created virtual environments on macOS now are excluded from Time Machine backups ([#4599](https://github.com/python-poetry/poetry/pull/4599)) - Poetry no longer raises an exception when a package is not found on PyPI ([#5698](https://github.com/python-poetry/poetry/pull/5698)) - Update `packaging` dependency to use major version 21, this change forces Poetry to drop support for managing Python 2.7 environments ([#4749](https://github.com/python-poetry/poetry/pull/4749)) ### Fixed - Fixed `poetry update --dry-run` to not modify `poetry.lock` ([#5718](https://github.com/python-poetry/poetry/pull/5718), [#3666](https://github.com/python-poetry/poetry/issues/3666), [#3766](https://github.com/python-poetry/poetry/issues/3766)) - Fixed [#5537](https://github.com/python-poetry/poetry/issues/5537) where export fails to resolve dependencies with more than one path ([#5688](https://github.com/python-poetry/poetry/pull/5688)) - Fixed an issue where the environment variables `POETRY_CONFIG_DIR` and `POETRY_CACHE_DIR` were not being respected ([#5672](https://github.com/python-poetry/poetry/pull/5672)) - Fixed [#3628](https://github.com/python-poetry/poetry/issues/3628) and [#4702](https://github.com/python-poetry/poetry/issues/4702) by handling invalid distributions gracefully ([#5645](https://github.com/python-poetry/poetry/pull/5645)) - Fixed an issue where the provider ignored subdirectory when merging and improve subdirectory support for vcs deps ([#5648](https://github.com/python-poetry/poetry/pull/5648)) - Fixed an issue where users could not select an empty choice when selecting dependencies ([#4606](https://github.com/python-poetry/poetry/pull/4606)) - Fixed an issue where `poetry init -n` crashes in a root directory ([#5612](https://github.com/python-poetry/poetry/pull/5612)) - Fixed an issue where Solver errors arise due to wheels having different Python constraints ([#5616](https://github.com/python-poetry/poetry/pull/5616)) - Fixed an issue where editable path dependencies using `setuptools` could not be correctly installed ([#5590](https://github.com/python-poetry/poetry/pull/5590)) - Fixed flicker when displaying executor operations ([#5556](https://github.com/python-poetry/poetry/pull/5556)) - Fixed an issue where the `poetry lock --no-update` only sorted by name and not by name and version ([#5446](https://github.com/python-poetry/poetry/pull/5446)) - Fixed an issue where the Solver fails when a dependency has multiple constrained dependency definitions for the same package ([#5403](https://github.com/python-poetry/poetry/pull/5403)) - Fixed an issue where dependency resolution takes a while because Poetry checks all possible combinations even markers are mutually exclusive ([#4695](https://github.com/python-poetry/poetry/pull/4695)) - Fixed incorrect version selector constraint ([#5500](https://github.com/python-poetry/poetry/pull/5500)) - Fixed an issue where `poetry lock --no-update` dropped packages ([#5435](https://github.com/python-poetry/poetry/pull/5435)) - Fixed an issue where packages were incorrectly grouped when exporting ([#5156](https://github.com/python-poetry/poetry/pull/5156)) - Fixed an issue where lockfile always updates when using private sources ([#5362](https://github.com/python-poetry/poetry/pull/5362)) - Fixed an issue where the solver did not account for selected package features ([#5305](https://github.com/python-poetry/poetry/pull/5305)) - Fixed an issue with console script execution of editable dependencies on Windows ([#3339](https://github.com/python-poetry/poetry/pull/3339)) - Fixed an issue where editable builder did not write PEP-610 metadata ([#5703](https://github.com/python-poetry/poetry/pull/5703)) - Fixed an issue where Poetry 1.1 lock files were incorrectly identified as not fresh ([#5458](https://github.com/python-poetry/poetry/pull/5458)) ### Docs - Updated plugin management commands ([#5450](https://github.com/python-poetry/poetry/pull/5450)) - Added the `--readme` flag to documentation ([#5357](https://github.com/python-poetry/poetry/pull/5357)) - Added example for multiple maintainers ([#5661](https://github.com/python-poetry/poetry/pull/5661)) - Updated documentation for issues [#4800](https://github.com/python-poetry/poetry/issues/4800), [#3709](https://github.com/python-poetry/poetry/issues/3709), [#3573](https://github.com/python-poetry/poetry/issues/3573), [#2211](https://github.com/python-poetry/poetry/issues/2211) and [#2414](https://github.com/python-poetry/poetry/pull/2414) ([#5656](https://github.com/python-poetry/poetry/pull/5656)) - Added `poetry.toml` note in configuration ([#5492](https://github.com/python-poetry/poetry/pull/5492)) - Add documentation for `poetry about`, `poetry help`, `poetrylist`, and the `--full-path` and `--all` options documentation ([#5664](https://github.com/python-poetry/poetry/pull/5664)) - Added more clarification to the `--why` flag ([#5653](https://github.com/python-poetry/poetry/pull/5653)) - Updated documentation to refer to PowerShell for Windows, including instructions ([#3978](https://github.com/python-poetry/poetry/pull/3978), [#5618](https://github.com/python-poetry/poetry/pull/5618)) - Added PEP 508 name requirement ([#5642](https://github.com/python-poetry/poetry/pull/5642)) - Added example for each section of pyproject.toml ([#5585](https://github.com/python-poetry/poetry/pull/5642)) - Added documentation for `--local` to fix issue [#5623](https://github.com/python-poetry/poetry/issues/5623) ([#5629](https://github.com/python-poetry/poetry/pull/5629)) - Added troubleshooting documentation for using proper quotation with ZSH ([#4847](https://github.com/python-poetry/poetry/pull/4847)) - Added information on git and basic http auth ([#5578](https://github.com/python-poetry/poetry/pull/5578)) - Removed ambiguity about PEP 440 and semver ([#5576](https://github.com/python-poetry/poetry/pull/5576)) - Removed Pipenv comparison ([#5561](https://github.com/python-poetry/poetry/pull/5561)) - Improved dependency group related documentation ([#5338](https://github.com/python-poetry/poetry/pull/5338)) - Added documentation for default directories used by Poetry ([#5391](https://github.com/python-poetry/poetry/pull/5301)) - Added warning about credentials preserved in shell history ([#5726](https://github.com/python-poetry/poetry/pull/5726)) - Improved documentation of the `readme` option, including multiple files and additional formats ([#5158](https://github.com/python-poetry/poetry/pull/5158)) - Improved contributing documentation ([#5708](https://github.com/python-poetry/poetry/pull/5708)) - Remove all references to `--dev-only` option ([#5771](https://github.com/python-poetry/poetry/pull/5771)) ## [1.2.0b1] - 2022-03-17 ### Fixed - Fixed an issue where the system environment couldn't be detected ([#4406](https://github.com/python-poetry/poetry/pull/4406)). - Fixed another issue where the system environment couldn't be detected ([#4433](https://github.com/python-poetry/poetry/pull/4433)). - Replace deprecated requests parameter in uploader ([#4580](https://github.com/python-poetry/poetry/pull/4580)). - Fix an issue where venv are detected as broken when using MSys2 on windows ([#4482](https://github.com/python-poetry/poetry/pull/4482)). - Fixed an issue where the cache breaks on windows ([#4531](https://github.com/python-poetry/poetry/pull/4531)). - Fixed an issue where a whitespace before a semicolon was missing on `poetry export` ([#4575](https://github.com/python-poetry/poetry/issues/4575)). - Fixed an issue where markers were not correctly assigned to nested dependencies ([#3511](https://github.com/python-poetry/poetry/issues/3511)). - Recognize one digit version in wheel filenames ([#3338](https://github.com/python-poetry/poetry/pull/3338)). - Fixed an issue when `locale` is unset ([#4038](https://github.com/python-poetry/poetry/pull/4038)). - Fixed an issue where the fallback to another interpreter didn't work ([#3475](https://github.com/python-poetry/poetry/pull/3475)). - Merge any marker constraints into constraints with specific markers ([#4590](https://github.com/python-poetry/poetry/pull/4590)). - Normalize path before hashing so that the generated venv name is independent of case on Windows ([#4813](https://github.com/python-poetry/poetry/pull/4813)). - Fixed an issue where a dependency wasn't upgrade by using `@latest` on `poetry update` ([#4945](https://github.com/python-poetry/poetry/pull/4945)). - Fixed an issue where conda envs in windows are always reported as broken([#5007](https://github.com/python-poetry/poetry/pull/5007)). - Fixed an issue where Poetry doesn't find its own venv on `poetry self update` ([#5049](https://github.com/python-poetry/poetry/pull/5049)). - Fix misuse of pretty_constraint ([#4932](https://github.com/python-poetry/poetry/pull/4932)). - Fixed an issue where the reported python version used for venv creation wasn't correct ([#5086](https://github.com/python-poetry/poetry/pull/5086)). - Fixed an issue where the searched package wasn't display in the interactive dialog of `poetry init` ([#5076](https://github.com/python-poetry/poetry/pull/5076)). - Fixed an issue where Poetry raises an exception on `poetry show` when no lock files exists ([#5242](https://github.com/python-poetry/poetry/pull/5242)). - Fixed an issue where Poetry crashes when optional `vcs_info.requested_version` in `direct_url.json` wasn't included ([#5274](https://github.com/python-poetry/poetry/pull/5274)). - Fixed an issue where dependencies with extras were updated despite using `--no-update` ([#4618](https://github.com/python-poetry/poetry/pull/4618)). - Fixed various places where poetry writes messages to stdout instead of stderr ([#4110](https://github.com/python-poetry/poetry/pull/4110), [#5179](https://github.com/python-poetry/poetry/pull/5179)). - Ensured that when complete packages are created dependency inherits source and resolved refs from package ([#4604](https://github.com/python-poetry/poetry/pull/4604)). - Ensured that when complete packages are created dependency inherits subdirectory from package if supported ([#4604](https://github.com/python-poetry/poetry/pull/4604)). - Fixed an issue where `POETRY_EXPERIMENTAL_NEW_INSTALLER` needs to be set to an empty string to disable it ([#3811](https://github.com/python-poetry/poetry/pull/3811)). ### Added - `poetry show ` now also shows which packages depend on it ([#2351](https://github.com/python-poetry/poetry/pull/2351)). - The info dialog by `poetry about` now contains version information about installed poetry and poetry-core ([#5288](https://github.com/python-poetry/poetry/pull/5288)). - Print error message when `poetry publish` fails ([#3549](https://github.com/python-poetry/poetry/pull/3549)). - Added in info output to `poetry lock --check` ([#5081](https://github.com/python-poetry/poetry/pull/5081)). - Added new argument `--all` for `poetry env remove` to delete all venv of a project at once ([#3212](https://github.com/python-poetry/poetry/pull/3212)). - Added new argument `--without-urls` for `poetry export` to exclude source repository urls from the exported file ([#4763](https://github.com/python-poetry/poetry/pull/4763)). - Added a new `installer.max-workers` property to the configuration ([#3516](https://github.com/python-poetry/poetry/pull/3516)). - Added experimental option `virtualenvs.prefer-active-python` to detect current activated python ([#4852](https://github.com/python-poetry/poetry/pull/4852)). - Added better windows shell support ([#5053](https://github.com/python-poetry/poetry/pull/5053)). ### Changed - Drop python3.6 support ([#5055](https://github.com/python-poetry/poetry/pull/5055)). - Exit with callable return code in generated script ([#4456](https://github.com/python-poetry/poetry/pull/4456)). - Internal use of the `pep517` high level interfaces for package metadata inspections have been replaced with the `build` package. ([#5155](https://github.com/python-poetry/poetry/pull/5155)). - Poetry now raises an error if the python version in the project environment is no longer compatible with the project ([#4520](https://github.com/python-poetry/poetry/pull/4520)). ## [1.1.13] - 2022-02-10 ### Fixed - Fixed an issue where envs in MSYS2 always reported as broken ([#4942](https://github.com/python-poetry/poetry/pull/4942)) - Fixed an issue where conda envs in windows are always reported as broken([#5008](https://github.com/python-poetry/poetry/pull/5008)) - Fixed an issue where Poetry doesn't find its own venv on `poetry self update` ([#5048](https://github.com/python-poetry/poetry/pull/5048)) ## [1.1.12] - 2021-11-27 ### Fixed - Fixed broken caches on Windows due to `Path` starting with a slash ([#4549](https://github.com/python-poetry/poetry/pull/4549)) - Fixed `JSONDecodeError` when installing packages by updating `cachecontrol` version ([#4831](https://github.com/python-poetry/poetry/pull/4831)) - Fixed dropped markers in dependency walk ([#4686](https://github.com/python-poetry/poetry/pull/4686)) ## [1.1.11] - 2021-10-04 ### Fixed - Fixed errors when installing packages on Python 3.10. ([#4592](https://github.com/python-poetry/poetry/pull/4592)) - Fixed an issue where the wrong `git` executable could be used on Windows. ([python-poetry/poetry-core#213](https://github.com/python-poetry/poetry-core/pull/213)) - Fixed an issue where the Python 3.10 classifier was not automatically added. ([python-poetry/poetry-core#215](https://github.com/python-poetry/poetry-core/pull/215)) ## [1.1.10] - 2021-09-21 ### Fixed - Fixed an issue where non-sha256 hashes were not checked. ([#4529](https://github.com/python-poetry/poetry/pull/4529)) ## [1.1.9] - 2021-09-18 ### Fixed - Fixed a security issue where file hashes were not checked prior to installation. ([#4420](https://github.com/python-poetry/poetry/pull/4420), [#4444](https://github.com/python-poetry/poetry/pull/4444), [python-poetry/poetry-core#193](https://github.com/python-poetry/poetry-core/pull/193)) - Fixed the detection of the system environment when the setting `virtualenvs.create` is deactivated. ([#4507](https://github.com/python-poetry/poetry/pull/4507)) - Fixed an issue where unsafe parameters could be passed to `git` commands. ([python-poetry/poetry-core#203](https://github.com/python-poetry/poetry-core/pull/203)) - Fixed an issue where the wrong `git` executable could be used on Windows. ([python-poetry/poetry-core#205](https://github.com/python-poetry/poetry-core/pull/205)) ## [1.1.8] - 2021-08-19 ### Fixed - Fixed an error with repository prioritization when specifying secondary repositories. ([#4241](https://github.com/python-poetry/poetry/pull/4241)) - Fixed the detection of the system environment when the setting `virtualenvs.create` is deactivated. ([#4330](https://github.com/python-poetry/poetry/pull/4330), [#4407](https://github.com/python-poetry/poetry/pull/4407)) - Fixed the evaluation of relative path dependencies. ([#4246](https://github.com/python-poetry/poetry/pull/4246)) - Fixed environment detection for Python 3.10 environments. ([#4387](https://github.com/python-poetry/poetry/pull/4387)) - Fixed an error in the evaluation of `in/not in` markers ([python-poetry/poetry-core#189](https://github.com/python-poetry/poetry-core/pull/189)) ## [1.2.0a2] - 2021-08-01 ### Added - Poetry now supports dependency groups. ([#4260](https://github.com/python-poetry/poetry/pull/4260)) - The `install` command now supports a `--sync` option to synchronize the environment with the lock file. ([#4336](https://github.com/python-poetry/poetry/pull/4336)) ### Changed - Improved the way credentials are retrieved to better support keyring backends. ([#4086](https://github.com/python-poetry/poetry/pull/4086)) - The `--remove-untracked` option of the `install` command is now deprecated in favor of the new `--sync` option. ([#4336](https://github.com/python-poetry/poetry/pull/4336)) - The user experience when installing dependency groups has been improved. ([#4336](https://github.com/python-poetry/poetry/pull/4336)) ### Fixed - Fixed performance issues when resolving dependencies. ([#3839](https://github.com/python-poetry/poetry/pull/3839)) - Fixed an issue where transitive dependencies of directory or VCS dependencies were not installed or otherwise removed. ([#4202](https://github.com/python-poetry/poetry/pull/4202)) - Fixed the behavior of the `init` command in non-interactive mode. ([#2899](https://github.com/python-poetry/poetry/pull/2899)) - Fixed the detection of the system environment when the setting `virtualenvs.create` is deactivated. ([#4329](https://github.com/python-poetry/poetry/pull/4329)) - Fixed the display of possible solutions for some common errors. ([#4332](https://github.com/python-poetry/poetry/pull/4332)) ## [1.1.7] - 2021-06-25 **Note**: Lock files might need to be regenerated for the first fix below to take effect. You can use `poetry lock` to do so **without** the `--no-update` option. ### Changed - This release is compatible with the `install-poetry.py` installation script to ease the migration path from `1.1` releases to `1.2` releases. ([#4192](https://github.com/python-poetry/poetry/pull/4192)) ### Fixed - Fixed an issue where transitive dependencies of directory or VCS dependencies were not installed or otherwise removed. ([#4203](https://github.com/python-poetry/poetry/pull/4203)) - Fixed an issue where the combination of the `--tree` and `--no-dev` options for the show command was still displaying development dependencies. ([#3992](https://github.com/python-poetry/poetry/pull/3992)) ## [1.2.0a1] - 2021-05-21 This release is the first testing release of the upcoming 1.2.0 version. It **drops** support for Python 2.7 and 3.5. ### Added - Poetry now supports a plugin system to alter or expand Poetry's functionality. ([#3733](https://github.com/python-poetry/poetry/pull/3733)) - Poetry now supports [PEP 610](https://www.python.org/dev/peps/pep-0610/). ([#3876](https://github.com/python-poetry/poetry/pull/3876)) - Several configuration options to better control the way virtual environments are created are now available. ([#3157](https://github.com/python-poetry/poetry/pull/3157), [#3711](https://github.com/python-poetry/poetry/pull/3711)). - The `new` command now supports namespace packages. ([#2768](https://github.com/python-poetry/poetry/pull/2768)) - The `add` command now supports the `--editable` option to add packages in editable mode. ([#3940](https://github.com/python-poetry/poetry/pull/3940)) ### Changed - Python 2.7 and 3.5 are no longer supported. ([#3405](https://github.com/python-poetry/poetry/pull/3405)) - The usage of the `get-poetry.py` script is now deprecated and is replaced by the `install-poetry.py` script. ([#3706](https://github.com/python-poetry/poetry/pull/3706)) - Directory dependencies are now in non-develop mode by default. ([poetry-core#98](https://github.com/python-poetry/poetry-core/pull/98)) - Improved support for PEP 440 specific versions that do not abide by semantic versioning. ([poetry-core#140](https://github.com/python-poetry/poetry-core/pull/140)) - Improved the CLI experience and performance by migrating to the latest version of Cleo. ([#3618](https://github.com/python-poetry/poetry/pull/3618)) - Packages previously considered as unsafe (`pip`, `setuptools`, `wheels` and `distribute`) can now be managed as any other package. ([#2826](https://github.com/python-poetry/poetry/pull/2826)) - The `new` command now defaults to the Markdown format for README files. ([#2768](https://github.com/python-poetry/poetry/pull/2768)) ### Fixed - Fixed an error where command line options were not taken into account when using the `run` command. ([#3618](https://github.com/python-poetry/poetry/pull/3618)) - Fixed an error in the way custom repositories were resolved. ([#3406](https://github.com/python-poetry/poetry/pull/3406)) ## [1.1.6] - 2021-04-14 ### Fixed - Fixed export format for path dependencies. ([#3121](https://github.com/python-poetry/poetry/pull/3121)) - Fixed errors caused by environment modification when executing some commands. ([#3253](https://github.com/python-poetry/poetry/pull/3253)) - Fixed handling of wheel files with single-digit versions. ([#3338](https://github.com/python-poetry/poetry/pull/3338)) - Fixed an error when handling single-digit Python markers. ([poetry-core#156](https://github.com/python-poetry/poetry-core/pull/156)) - Fixed dependency markers not being properly copied when changing the constraint leading to resolution errors. ([poetry-core#163](https://github.com/python-poetry/poetry-core/pull/163)) - Fixed an error where VCS dependencies were always updated. ([#3947](https://github.com/python-poetry/poetry/pull/3947)) - Fixed an error where the incorrect version of a package was locked when using environment markers. ([#3945](https://github.com/python-poetry/poetry/pull/3945)) ## [1.1.5] - 2021-03-04 ### Fixed - Fixed an error in the `export` command when no lock file existed and a verbose flag was passed to the command. ([#3310](https://github.com/python-poetry/poetry/pull/3310)) - Fixed an error where the `pyproject.toml` was not reverted when using the `add` command. ([#3622](https://github.com/python-poetry/poetry/pull/3622)) - Fixed errors when using non-HTTPS indices. ([#3622](https://github.com/python-poetry/poetry/pull/3622)) - Fixed errors when handling simple indices redirection. ([#3622](https://github.com/python-poetry/poetry/pull/3622)) - Fixed errors when trying to handle newer wheels by using the latest version of `poetry-core` and `packaging`. ([#3677](https://github.com/python-poetry/poetry/pull/3677)) - Fixed an error when using some versions of `poetry-core` due to an incorrect import . ([#3696](https://github.com/python-poetry/poetry/pull/3696)) ## [1.1.4] - 2020-10-23 ### Added - Added `installer.parallel` boolean flag (defaults to `true`) configuration to enable/disable parallel execution of operations when using the new installer. ([#3088](https://github.com/python-poetry/poetry/pull/3088)) ### Changed - When using system environments as an unprivileged user, user site and bin directories are created if they do not already exist. ([#3107](https://github.com/python-poetry/poetry/pull/3107)) ### Fixed - Fixed editable installation of poetry projects when using system environments. ([#3107](https://github.com/python-poetry/poetry/pull/3107)) - Fixed locking of nested extra activations. If you were affected by this issue, you will need to regenerate the lock file using `poetry lock --no-update`. ([#3229](https://github.com/python-poetry/poetry/pull/3229)) - Fixed prioritisation of non-default custom package sources. ([#3251](https://github.com/python-poetry/poetry/pull/3251)) - Fixed detection of installed editable packages when non-poetry managed `.pth` file exists. ([#3210](https://github.com/python-poetry/poetry/pull/3210)) - Fixed scripts generated by editable builder to use valid import statements. ([#3214](https://github.com/python-poetry/poetry/pull/3214)) - Fixed recursion error when locked dependencies contain cyclic dependencies. ([#3237](https://github.com/python-poetry/poetry/pull/3237)) - Fixed propagation of editable flag for VCS dependencies. ([#3264](https://github.com/python-poetry/poetry/pull/3264)) ## [1.1.3] - 2020-10-14 ### Changed - Python version support deprecation warning is now written to `stderr`. ([#3131](https://github.com/python-poetry/poetry/pull/3131)) ### Fixed - Fixed `KeyError` when `PATH` is not defined in environment variables. ([#3159](https://github.com/python-poetry/poetry/pull/3159)) - Fixed error when using `config` command in a directory with an existing `pyproject.toml` without any Poetry configuration. ([#3172](https://github.com/python-poetry/poetry/pull/3172)) - Fixed incorrect inspection of package requirements when same dependency is specified multiple times with unique markers. ([#3147](https://github.com/python-poetry/poetry/pull/3147)) - Fixed `show` command to use already resolved package metadata. ([#3117](https://github.com/python-poetry/poetry/pull/3117)) - Fixed multiple issues with `export` command output when using `requirements.txt` format. ([#3119](https://github.com/python-poetry/poetry/pull/3119)) ## [1.1.2] - 2020-10-06 ### Changed - Dependency installation of editable packages and all uninstall operations are now performed serially within their corresponding priority groups. ([#3099](https://github.com/python-poetry/poetry/pull/3099)) - Improved package metadata inspection of nested poetry projects within project path dependencies. ([#3105](https://github.com/python-poetry/poetry/pull/3105)) ### Fixed - Fixed export of `requirements.txt` when project dependency contains git dependencies. ([#3100](https://github.com/python-poetry/poetry/pull/3100)) ## [1.1.1] - 2020-10-05 ### Added - Added `--no-update` option to `lock` command. ([#3034](https://github.com/python-poetry/poetry/pull/3034)) ### Fixed - Fixed resolution of packages with missing required extras. ([#3035](https://github.com/python-poetry/poetry/pull/3035)) - Fixed export of `requirements.txt` dependencies to include development dependencies. ([#3024](https://github.com/python-poetry/poetry/pull/3024)) - Fixed incorrect selection of unsupported binary distribution formats when selecting a package artifact to install. ([#3058](https://github.com/python-poetry/poetry/pull/3058)) - Fixed incorrect use of system executable when building package distributions via `build` command. ([#3056](https://github.com/python-poetry/poetry/pull/3056)) - Fixed errors in `init` command when specifying `--dependency` in non-interactive mode when a `pyproject.toml` file already exists. ([#3076](https://github.com/python-poetry/poetry/pull/3076)) - Fixed incorrect selection of configured source url when a publish repository url configuration with the same name already exists. ([#3047](https://github.com/python-poetry/poetry/pull/3047)) - Fixed dependency resolution issues when the same package is specified in multiple dependency extras. ([#3046](https://github.com/python-poetry/poetry/pull/3046)) ## [1.1.0] - 2020-10-01 ### Changed - The `init` command will now use existing `pyproject.toml` if possible ([#2448](https://github.com/python-poetry/poetry/pull/2448)). - Error messages when metadata information retrieval fails have been improved ([#2997](https://github.com/python-poetry/poetry/pull/2997)). ### Fixed - Fixed parsing of version constraint for `rc` prereleases ([#2978](https://github.com/python-poetry/poetry/pull/2978)). - Fixed how some metadata information are extracted from `setup.cfg` files ([#2957](https://github.com/python-poetry/poetry/pull/2957)). - Fixed return codes returned by the executor ([#2981](https://github.com/python-poetry/poetry/pull/2981)). - Fixed whitespaces not being accepted for the list of extras when adding packages ([#2985](https://github.com/python-poetry/poetry/pull/2985)). - Fixed repositories specified in the `pyproject.toml` file not being taken into account for authentication when downloading packages ([#2990](https://github.com/python-poetry/poetry/pull/2990)). - Fixed permission errors when installing the root project if the `site-packages` directory is not writeable ([#3002](https://github.com/python-poetry/poetry/pull/3002)). - Fixed environment marker propagation when exporting to the `requirements.txt` format ([#3002](https://github.com/python-poetry/poetry/pull/3002)). - Fixed errors when paths in run command contained spaces ([#3015](https://github.com/python-poetry/poetry/pull/3015)). ## [1.1.0rc1] - 2020-09-25 ### Changed - The `virtualenvs.in-project` setting will now always be honored, if set explicitly, regardless of the presence of a `.venv` directory ([#2771](https://github.com/python-poetry/poetry/pull/2771)). - Adding packages already present in the `pyproject.toml` file will no longer raise an error ([#2886](https://github.com/python-poetry/poetry/pull/2886)). - Errors when authenticating against custom repositories will now be logged ([#2577](https://github.com/python-poetry/poetry/pull/2577)). ### Fixed - Fixed an error on Python 3.5 when resolving URL dependencies ([#2954](https://github.com/python-poetry/poetry/pull/2954)). - Fixed the `dependency` option of the `init` command being ignored ([#2587](https://github.com/python-poetry/poetry/pull/2587)). - Fixed the `show` command displaying erroneous information following the changes in the lock file format ([#2967](https://github.com/python-poetry/poetry/pull/2967)). - Fixed dependency resolution errors due to invalid python constraints propagation ([#2968](https://github.com/python-poetry/poetry/pull/2968)). ## [1.1.0b4] - 2020-09-23 ### Changed - When running under Python 2.7 on Windows, install command will be limited to one worker to mitigate threading issue ([#2941](https://github.com/python-poetry/poetry/pull/2941)). ## [1.1.0b3] - 2020-09-18 ### Changed - Improved the error reporting when HTTP error are encountered for legacy repositories ([#2459](https://github.com/python-poetry/poetry/pull/2459)). - When displaying the name of packages retrieved from remote repositories, the original name will now be used ([#2305](https://github.com/python-poetry/poetry/pull/2305)). - Failed package downloads will now be retried on connection errors ([#2813](https://github.com/python-poetry/poetry/pull/2813)). - Path dependencies will now be installed as editable only when `develop` option is set to `true` ([#2887](https://github.com/python-poetry/poetry/pull/2887)). ### Fixed - Fixed the detection of the type of installed packages ([#2722](https://github.com/python-poetry/poetry/pull/2722)). - Fixed deadlocks when installing packages on systems not supporting non-ascii characters ([#2721](https://github.com/python-poetry/poetry/pull/2721)). - Fixed handling of wildcard constraints for packages with prereleases only ([#2821](https://github.com/python-poetry/poetry/pull/2821)). - Fixed dependencies of some packages not being discovered by ensuring we use the PEP-516 backend if specified ([#2810](https://github.com/python-poetry/poetry/pull/2810)). - Fixed recursion errors when retrieving extras ([#2787](https://github.com/python-poetry/poetry/pull/2787)). - Fixed `PyPI` always being displayed when publishing even for custom repositories ([#2905](https://github.com/python-poetry/poetry/pull/2905)). - Fixed handling of packages extras when resolving dependencies ([#2887](https://github.com/python-poetry/poetry/pull/2887)). ## [1.1.0b2] - 2020-07-24 ### Changed - Added support for build scripts without the `setup.py` file generation in the editable builder ([#2718](https://github.com/python-poetry/poetry/pull/2718)). ### Fixed - Fixed an error occurring when using older lock files ([#2717](https://github.com/python-poetry/poetry/pull/2717)). ## [1.1.0b1] - 2020-07-24 ### Changed - Virtual environments will now exclusively be built with `virtualenv` ([#2666](https://github.com/python-poetry/poetry/pull/2666)). - Support for Python 2.7 and 3.5 is now officially deprecated and a warning message will be displayed ([#2683](https://github.com/python-poetry/poetry/pull/2683)). - Improved metadata inspection of packages by using the PEP-517 build system ([#2632](https://github.com/python-poetry/poetry/pull/2632)). ### Fixed - Fixed parallel tasks not being cancelled when the installation is interrupted or has failed ([#2656](https://github.com/python-poetry/poetry/pull/2656)). - Fixed an error where the editable builder would not expose all packages ([#2664](https://github.com/python-poetry/poetry/pull/2656)). - Fixed an error for Python 2.7 when a file could not be downloaded in the installer ([#2709](https://github.com/python-poetry/poetry/pull/2709)). - Fixed the lock file `content-hash` value not being updated when using the `add` and `remove` commands ([#2710](https://github.com/python-poetry/poetry/pull/2710)). - Fixed incorrect resolution errors being raised for packages with python requirements ([#2712](https://github.com/python-poetry/poetry/pull/2712)). - Fixed an error causing the build log messages to no longer be displayed ([#2715](https://github.com/python-poetry/poetry/pull/2715)). ## [1.0.10] - 2020-07-21 ### Changed - The lock files are now versioned to ease transitions for lock file format changes, with warnings being displayed on incompatibility detection ([#2695](https://github.com/python-poetry/poetry/pull/2695)). - The `init` and `new` commands will now provide hints on invalid given licenses ([#1634](https://github.com/python-poetry/poetry/pull/1634)). ### Fixed - Fixed error messages when the authors specified in the `pyproject.toml` file are invalid ([#2525](https://github.com/python-poetry/poetry/pull/2525)). - Fixed empty `.venv` directories being deleted ([#2064](https://github.com/python-poetry/poetry/pull/2064)). - Fixed the `shell` command for `tcsh` shells ([#2583](https://github.com/python-poetry/poetry/pull/2583)). - Fixed errors when installing directory or file dependencies in some cases ([#2582](https://github.com/python-poetry/poetry/pull/2582)). ## [1.1.0a3] - 2020-07-10 ### Added - New installer which provides a faster and better experience ([#2595](https://github.com/python-poetry/poetry/pull/2595)). ### Fixed - Fixed resolution error when handling duplicate dependencies with environment markers ([#2622](https://github.com/python-poetry/poetry/pull/2622)). - Fixed erroneous resolution errors when resolving packages to install ([#2625](https://github.com/python-poetry/poetry/pull/2625)). - Fixed errors when detecting installed editable packages ([#2602](https://github.com/python-poetry/poetry/pull/2602)). ## [1.1.0a2] - 2020-06-26 Note that lock files generated with this release are not compatible with previous releases of Poetry. ### Added - The `install` command now supports a `--remove-untracked` option to ensure only packages from the lock file are present in the environment ([#2172](https://github.com/python-poetry/poetry/pull/2172)). - Some errors will now be provided with possible solutions and links to the documentation ([#2396](https://github.com/python-poetry/poetry/pull/2396)). ### Changed - Editable installations of Poetry projects have been improved and are now faster ([#2360](https://github.com/python-poetry/poetry/pull/2360)). - Improved the accuracy of the dependency resolver in case of dependencies with environment markers ([#2361](https://github.com/python-poetry/poetry/pull/2361)) - Environment markers of dependencies are no longer stored in the lock file ([#2361](https://github.com/python-poetry/poetry/pull/2361)). - Improved the way connection errors are handled when publishing ([#2285](https://github.com/python-poetry/poetry/pull/2285)). ### Fixed - Fixed errors when handling duplicate dependencies with environment markers ([#2342](https://github.com/python-poetry/poetry/pull/2342)). - Fixed the detection of installed packages ([#2360](https://github.com/python-poetry/poetry/pull/2360)). ## [1.1.0a1] - 2020-03-27 This release **must** be downloaded via the `get-poetry.py` script and not via the `self update` command. ### Added - Added a new `--dry-run` option to the `publish` command ([#2199](https://github.com/python-poetry/poetry/pull/2199)). ### Changed - The core features of Poetry have been extracted in to a separate library: `poetry-core` ([#2212](https://github.com/python-poetry/poetry/pull/2212)). - The build backend is no longer `poetry.masonry.api` but `poetry.core.masonry.api` which requires `poetry-core>=1.0.0a5` ([#2212](https://github.com/python-poetry/poetry/pull/2212)). - The exceptions are now beautifully displayed in the terminal with various level of details depending on the verbosity ([2230](https://github.com/python-poetry/poetry/pull/2230)). ## [1.0.9] - 2020-06-09 ### Fixed - Fixed an issue where packages from custom indices where continuously updated ([#2525](https://github.com/python-poetry/poetry/pull/2525)). - Fixed errors in the way Python environment markers were parsed and generated ([#2526](https://github.com/python-poetry/poetry/pull/2526)). ## [1.0.8] - 2020-06-05 ### Fixed - Fixed a possible error when installing the root package ([#2505](https://github.com/python-poetry/poetry/pull/2505)). - Fixed an error where directory and VCS dependencies were not installed ([#2505](https://github.com/python-poetry/poetry/pull/2505)). ## [1.0.7] - 2020-06-05 ### Fixed - Fixed an error when trying to execute some packages `setup.py` file ([#2349](https://github.com/python-poetry/poetry/pull/2349)). ## [1.0.6] - 2020-06-05 ### Changed - The `self update` command has been updated in order to handle future releases of Poetry ([#2429](https://github.com/python-poetry/poetry/pull/2429)). ### Fixed - Fixed an error were a new line was not written when displaying the virtual environment's path with `env info` ([#2196](https://github.com/python-poetry/poetry/pull/2196)). - Fixed a misleading error message when the `packages` property was empty ([#2265](https://github.com/python-poetry/poetry/pull/2265)). - Fixed shell detection by using environment variables ([#2147](https://github.com/python-poetry/poetry/pull/2147)). - Fixed the removal of VCS dependencies ([#2239](https://github.com/python-poetry/poetry/pull/2239)). - Fixed generated wheel ABI tags for Python 3.8 ([#2121](https://github.com/python-poetry/poetry/pull/2121)). - Fixed a regression when building stub-only packages ([#2000](https://github.com/python-poetry/poetry/pull/2000)). - Fixed errors when parsing PEP-440 constraints with whitespace ([#2347](https://github.com/python-poetry/poetry/pull/2347)). - Fixed PEP 508 representation of VCS dependencies ([#2349](https://github.com/python-poetry/poetry/pull/2349)). - Fixed errors when source distributions were read-only ([#1140](https://github.com/python-poetry/poetry/pull/1140)). - Fixed dependency resolution errors and inconsistencies with directory, file and VCS dependencies ([#2398](https://github.com/python-poetry/poetry/pull/2398)). - Fixed custom repositories information not being properly locked ([#2484](https://github.com/python-poetry/poetry/pull/2484)). ## [1.0.5] - 2020-02-29 ### Fixed - Fixed an error when building distributions if the `git` executable was not found ([#2105](https://github.com/python-poetry/poetry/pull/2105)). - Fixed various errors when reading Poetry's TOML files by upgrading [tomlkit](https://github.com/sdispater/tomlkit). ## [1.0.4] - 2020-02-28 ### Fixed - Fixed the PyPI URL used when installing packages ([#2099](https://github.com/python-poetry/poetry/pull/2099)). - Fixed errors when the author's name contains special characters ([#2006](https://github.com/python-poetry/poetry/pull/2006)). - Fixed VCS excluded files detection when building wheels ([#1947](https://github.com/python-poetry/poetry/pull/1947)). - Fixed packages detection when building sdists ([#1626](https://github.com/python-poetry/poetry/pull/1626)). - Fixed the local `.venv` virtual environment not being displayed in `env list` ([#1762](https://github.com/python-poetry/poetry/pull/1762)). - Fixed incompatibilities with the most recent versions of `virtualenv` ([#2096](https://github.com/python-poetry/poetry/pull/2096)). - Fixed Poetry's own vendor dependencies being retrieved when updating dependencies ([#1981](https://github.com/python-poetry/poetry/pull/1981)). - Fixed encoding of credentials in URLs ([#1911](https://github.com/python-poetry/poetry/pull/1911)). - Fixed url constraints not being accepted in multi-constraints dependencies ([#2035](https://github.com/python-poetry/poetry/pull/2035)). - Fixed an error where credentials specified via environment variables were not retrieved ([#2061](https://github.com/python-poetry/poetry/pull/2061)). - Fixed an error where git dependencies referencing tags were not locked to the corresponding commit ([#1948](https://github.com/python-poetry/poetry/pull/1948)). - Fixed an error when parsing packages `setup.py` files ([#2041](https://github.com/python-poetry/poetry/pull/2041)). - Fixed an error when parsing some git URLs ([#2018](https://github.com/python-poetry/poetry/pull/2018)). ## [1.0.3] - 2020-01-31 ### Fixed - Fixed an error which caused the configuration environment variables (like `POETRY_HTTP_BASIC_XXX_PASSWORD`) to not be used ([#1909](https://github.com/python-poetry/poetry/pull/1909)). - Fixed an error where the `--help` option was not working ([#1910](https://github.com/python-poetry/poetry/pull/1910)). - Fixed an error where packages from private indices were not decompressed properly ([#1851](https://github.com/python-poetry/poetry/pull/1851)). - Fixed an error where the version of some PEP-508-formatted wheel dependencies was not properly retrieved ([#1932](https://github.com/python-poetry/poetry/pull/1932)). - Fixed internal regexps to avoid potential catastrophic backtracking errors ([#1913](https://github.com/python-poetry/poetry/pull/1913)). - Fixed performance issues when custom indices were defined in the `pyproject.toml` file ([#1892](https://github.com/python-poetry/poetry/pull/1892)). - Fixed the `get_requires_for_build_wheel()` function of `masonry.api` which wasn't returning the proper result ([#1875](https://github.com/python-poetry/poetry/pull/1875)). ## [1.0.2] - 2020-01-10 ### Fixed - Reverted a previous fix ([#1796](https://github.com/python-poetry/poetry/pull/1796)) which was causing errors for projects with file and/or directory dependencies ([#1865](https://github.com/python-poetry/poetry/pull/1865)). ## [1.0.1] - 2020-01-10 ### Fixed - Fixed an error in `env use` where the wrong Python executable was being used to check compatibility ([#1736](https://github.com/python-poetry/poetry/pull/1736)). - Fixed an error where VCS dependencies were not properly categorized as development dependencies ([#1725](https://github.com/python-poetry/poetry/pull/1725)). - Fixed an error where some shells would no longer be usable after using the `shell` command ([#1673](https://github.com/python-poetry/poetry/pull/1673)). - Fixed an error where explicitly included files where not included in wheel distributions ([#1750](https://github.com/python-poetry/poetry/pull/1750)). - Fixed an error where some Git dependencies url were not properly parsed ([#1756](https://github.com/python-poetry/poetry/pull/1756)). - Fixed an error in the `env` commands on Windows if the path to the executable contained a space ([#1774](https://github.com/python-poetry/poetry/pull/1774)). - Fixed several errors and UX issues caused by `keyring` on some systems ([#1788](https://github.com/python-poetry/poetry/pull/1788)). - Fixed errors when trying to detect installed packages ([#1786](https://github.com/python-poetry/poetry/pull/1786)). - Fixed an error when packaging projects where Python packages were not properly detected ([#1592](https://github.com/python-poetry/poetry/pull/1592)). - Fixed an error where local file dependencies were exported as editable when using the `export` command ([#1840](https://github.com/python-poetry/poetry/pull/1840)). - Fixed the way environment markers are propagated and evaluated when resolving dependencies ([#1829](https://github.com/python-poetry/poetry/pull/1829), [#1789](https://github.com/python-poetry/poetry/pull/1789)). - Fixed an error in the PEP-508 compliant representation of directory and file dependencies ([#1796](https://github.com/python-poetry/poetry/pull/1796)). - Fixed an error where invalid virtual environments would be silently used. They will not be recreated and a warning will be displayed ([#1797](https://github.com/python-poetry/poetry/pull/1797)). - Fixed an error where dependencies were not properly detected when reading the `setup.py` file in some cases ([#1764](https://github.com/python-poetry/poetry/pull/1764)). ## [1.0.0] - 2019-12-12 ### Added - Added an `export` command to export the lock file to other formats (only `requirements.txt` is currently supported). - Added a `env info` command to get basic information about the current environment. - Added a `env use` command to control the Python version used by the project. - Added a `env list` command to list the virtualenvs associated with the current project. - Added a `env remove` command to delete virtualenvs associated with the current project. - Added support for `POETRY_HOME` declaration within `get-poetry.py`. - Added support for declaring a specific source for dependencies. - Added support for disabling PyPI and making another repository the default one. - Added support for declaring private repositories as secondary. - Added the ability to specify packages on a per-format basis. - Added support for custom urls in metadata. - Full environment markers are now supported for dependencies via the `markers` property. - Added the ability to specify git dependencies directly in `add`, it no longer requires the `--git` option. - Added the ability to specify path dependencies directly in `add`, it no longer requires the `--path` option. - Added support for url dependencies ([#1260](https://github.com/python-poetry/poetry/pull/1260)). - Publishing to PyPI using [API tokens](https://pypi.org/help/#apitoken) is now supported ([#1275](https://github.com/python-poetry/poetry/pull/1275)). - Licenses can now be identified by their full name. - Added support for custom certificate authority and client certificates for private repositories. - Poetry can now detect and use Conda environments. ### Changed - Slightly changed the lock file, making it potentially incompatible with previous Poetry versions. - The `cache:clear` command has been renamed to `cache clear`. - The `debug:info` command has been renamed to `debug info`. - The `debug:resolve` command has been renamed to `debug resolve`. - The `self:update` command has been renamed to `self update`. - Changed the way virtualenvs are stored (names now depend on the project's path). - The `--git` option of the `add` command has been removed. - The `--path` option of the `add` command has been removed. - The `add` command will now automatically select the latest prerelease if only prereleases are available. - The `add` command can now update a dependencies if an explicit constraint is given ([#1221](https://github.com/python-poetry/poetry/pull/1221)). - Removed the `--develop` option from the `install` command. - Improved UX when searching for packages in the `init` command. - The `shell` command has been improved. - The `poetry run` command now uses `os.execvp()` rather than spawning a new subprocess. - Specifying dependencies with `allows-prereleases` in the `pyproject.toml` file is deprecated for consistency with the `add` command. Use `allow-prereleases` instead. - Improved the error message when the lock file is invalid. - Whenever Poetry needs to use the "system" Python, it will now call `sys.executable` instead of the `python` command. - Improved the error message displayed on conflicting Python requirements ([#1681](https://github.com/python-poetry/poetry/pull/1681)). - Improved the `site-packages` directory detection ([#1683](https://github.com/python-poetry/poetry/pull/1683)). ### Fixed - Fixed transitive extra dependencies being removed when updating a specific dependency. - The `pyproject.toml` configuration is now properly validated. - Fixed installing Poetry-based packages breaking with `pip`. - Fixed packages with empty markers being added to the lock file. - Fixed invalid lock file generation in some cases. - Fixed local version identifier handling in wheel file names. - Fixed packages with invalid metadata triggering an error instead of being skipped. - Fixed the generation of invalid lock files in some cases. - Git dependencies are now properly locked to a specific revision when specifying a branch or a tag. - Fixed the behavior of the `~=` operator. - Fixed dependency resolution for conditional development dependencies. - Fixed generated dependency constraints when they contain inequality operators. - The `run` command now properly handles the `--` separator. - Fixed some issues with `path` dependencies being seen as `git` dependencies. - Fixed various issues with the way `extra` markers in dependencies were handled. - Fixed the option conflicts in the `run` command. - Fixed wrong latest version being displayed when executing `show -l`. - Fixed `TooManyRedirects` errors being raised when resolving dependencies. - Fixed custom indices dependencies being constantly updated. - Fixed the behavior of the `--install` option of the debug resolve command. - Fixed an error in `show` when using the `-o/--outdated` option. - Fixed PEP 508 url dependency handling. - Fixed excluded files via the `exclude` being included in distributions. - Fixed an error in `env use` if the `virtualenvs.in-project` setting is activated ([#1682](https://github.com/python-poetry/poetry/pull/1682)) - Fixed handling of `empty` and `any` markers in unions of markers ([#1650](https://github.com/python-poetry/poetry/pull/1650)). ## [0.12.17] - 2019-07-03 ### Fixed - Fixed dependency resolution with circular dependencies. - Fixed encoding errors when reading files on Windows. (Thanks to [@vlcinsky](https://github.com/vlcinsky)) - Fixed unclear errors when executing commands in virtual environments. (Thanks to [@Imaclean74](https://github.com/Imaclean74)) - Fixed handling of `.venv` when it's not a directory. (Thanks to [@mpanarin](https://github.com/mpanarin)) ## [0.12.16] - 2019-05-17 ### Fixed - Fixed packages with no hashes retrieval for legacy repositories. - Fixed multiple constraints for dev dependencies. - Fixed dependency resolution failing on badly formed package versions instead of skipping. - Fixed permissions of built wheels. ## [0.12.15] - 2019-05-03 ### Fixed - Fixed an `AttributeError` in the editable builder. - Fixed resolution of packages with only Python 3 wheels and sdist when resolving for legacy repositories. - Fixed non-sha256 hashes retrieval for legacy repositories. ## [0.12.14] - 2019-04-26 ### Fixed - Fixed root package installation for pure Python packages. ## [0.12.13] - 2019-04-26 ### Fixed - Fixed root package installation with `pip>=19.0`. - Fixed packages not being removed after using the `remove` command. ## [0.12.12] - 2019-04-11 ### Fixed - Fix lock idempotency. - Fix markers evaluation for `python_version` with precision < 3. - Fix permissions of the `dist-info` files. - Fix `prepare_metadata_for_build_wheel()` missing in the build backend. - Fix metadata inconsistency between wheels and sdists. - Fix parsing of `platform_release` markers. - Fix metadata information when the project has git dependencies. - Fix error reporting when publishing fails. - Fix retrieval of `extras_require` in some `setup.py` files. (Thanks to [@asodeur](https://github.com/asodeur)) - Fix wheel compression when building. (Thanks to [@ccosby](https://github.com/ccosby)) - Improve retrieval of information for packages with two python specific wheels. - Fix request authentication when credentials are included in URLs. (Thanks to [@connorbrinton](https://github.com/connorbrinton)) ## [0.12.11] - 2019-01-13 ### Fixed - Fixed the way packages information are retrieved for legacy repositories. - Fixed an error when adding packages with invalid versions. - Fixed an error when resolving directory dependencies with no sub dependencies. - Fixed an error when locking packages with no description. - Fixed path resolution for transitive file dependencies. - Fixed multiple constraints handling for the root package. - Fixed exclude functionality on case sensitive systems. ## [0.12.10] - 2018-11-22 ### Fixed - Fixed `run` not executing scripts. - Fixed environment detection. - Fixed handling of authentication for legacy repositories. ## [0.12.9] - 2018-11-19 ### Fixed - Fixed executables from outside the virtualenv not being accessible. - Fixed a possible error when building distributions with the `exclude` option. - Fixed the `run` command for namespaced packages. - Fixed errors for virtualenvs with spaces in their path. - Fixed prerelease versions being selected with the `add` command. ## [0.12.8] - 2018-11-13 ### Fixed - Fixed permission errors when adding/removing git dependencies on Windows. - Fixed `Pool` not raising an exception when no package could be found. - Fixed reading `bz2` source distribution. - Fixed handling of arbitrary equals in `InstalledRepository`. ## [0.12.7] - 2018-11-08 ### Fixed - Fixed reading of some `setup.py` files. - Fixed a `KeyError` when getting information for packages which require reading setup files. - Fixed the building of wheels with C extensions and an `src` layout. - Fixed extras being selected when resolving dependencies even when not required. - Fixed performance issues when packaging projects if a lot of files were excluded. - Fixed installation of files. - Fixed extras not being retrieved for legacy repositories. - Fixed invalid transitive constraints raising an error for legacy repositories. ## [0.12.6] - 2018-11-05 ### Changed - Poetry will now try to read, without executing, setup files (`setup.py` and/or `setup.cfg`) if the `egg_info` command fails when resolving dependencies. ### Fixed - Fixed installation of directory dependencies. - Fixed handling of dependencies with a `not in` marker operator. - Fixed support for VCS dependencies. - Fixed the `exclude` property not being respected if no VCS was available. ## [0.12.5] - 2018-10-26 ### Fixed - Fixed installation of Poetry git dependencies with a build system. - Fixed possible errors when resolving dependencies for specific packages. - Fixed handling of Python versions compatibility. - Fixed the dependency resolver picking up unnecessary dependencies due to not using the `python_full_version` marker. - Fixed the `Python-Requires` metadata being invalid for single Python versions. ## [0.12.4] - 2018-10-21 ### Fixed - Fixed possible error on some combinations of markers. - Fixed venv detection so that it only uses `VIRTUAL_ENV` to detect activated virtualenvs. ## [0.12.3] - 2018-10-18 ### Fixed - Fixed the `--no-dev` option in `install` not working properly. - Fixed prereleases being selected even if another constraint conflicted with them. - Fixed an error when installing current package in development mode if the generated `setup.py` had special characters. - Fixed an error in `install` for applications not following a known structure. - Fixed an error when trying to retrieve the current environment. - Fixed `debug:info` not showing the current project's virtualenv. ## [0.12.2] - 2018-10-17 ### Fixed - Fixed an error when installing from private repositories. - Fixed an error when trying to move the lock file on Python 2.7. ## [0.12.1] - 2018-10-17 ### Fixed - Fixed an error when license is unspecified. ## [0.12.0] - 2018-10-17 ### Added - Added a brand new installer. - Added support for multi-constraints dependencies. - Added a cache version system. - Added a `--lock` option to `update` to only update the lock file without executing operations. (Thanks to [@greysteil](https://github.com/greysteil)) - Added support for the `Project-URL` metadata. - Added support for optional scripts. - Added a `--no-dev` option to `show`. (Thanks to [@rodcloutier](https://github.com/rodcloutier)) ### Changed - Improved virtualenv detection and management. - Wildcard `python` dependencies are now equivalent to `~2.7 || ^3.4`. - Changed behavior of the resolver for conditional dependencies. - The `install` command will now install the current project in editable mode. - The `develop` command is now deprecated in favor of `install`. - Improved the `check` command. - Empty passwords are now supported when publishing. ### Fixed - Fixed a memory leak in the resolver. - Fixed a recursion error on duplicate dependencies with only different extras. - Fixed handling of extras. - Fixed duplicate entries in both sdist and wheel. - Fixed excluded files appearing in the `package_data` of the generated `setup.py`. - Fixed transitive directory dependencies installation. - Fixed file permissions for configuration and authentication files. - Fixed an error in `cache:clear` for Python 2.7. - Fixed publishing for the first time with a prerelease. ## [0.11.5] - 2018-09-04 ### Fixed - Fixed a recursion error with circular dependencies. - Fixed the `config` command setting incorrect values for paths. - Fixed an `OSError` on Python >= 3.5 for `git` dependencies with recursive symlinks. - Fixed the possible deletion of system paths by `cache:clear`. - Fixed a performance issue when parsing the lock file by upgrading `tomlkit`. ## [0.11.4] - 2018-07-30 ### Fixed - Fixed wrong wheel being selected when resolving dependencies. - Fixed an error when publishing. - Fixed an error when building wheels with the `packages` property set. - Fixed single value display in `config` command. ## [0.11.3] - 2018-07-26 ### Changed - Poetry now only uses [TOML Kit](https://github.com/sdispater/tomlkit) for TOML files manipulation. - Improved dependency resolution debug information. ### Fixed - Fixed missing dependency information for some packages. - Fixed handling of single versions when packaging. - Fixed dependency information retrieval from `.zip` and `.bz2` archives. - Fixed searching for and installing packages from private repositories with authentication. (Thanks to [@MarcDufresne](https://github.com/MarcDufresne)) - Fixed a potential error when checking the `pyproject.toml` validity. (Thanks to [@ojii](https://github.com/ojii)) - Fixed the lock file not tracking the `extras` information from `pyproject.toml`. (Thanks to [@cauebs](https://github.com/cauebs)) - Fixed missing trailing slash in the Simple API urls for private repositories. (Thanks to [@bradsbrown](https://github.com/bradsbrown)) ## [0.11.2] - 2018-07-03 ### Fixed - Fixed missing dependencies when resolving in some cases. - Fixed path dependencies not working in `dev-dependencies`. - Fixed license validation in `init`. (Thanks to [@cauebs](https://github.com/cauebs)) ## [0.11.1] - 2018-06-29 ### Fixed - Fixed an error when locking dependencies on Python 2.7. ## [0.11.0] - 2018-06-28 ### Added - Added support for `packages`, `include` and `exclude` properties. - Added a new `shell` command. (Thanks to [@cauebs](https://github.com/cauebs)) - Added license validation in `init` command. ### Changed - Changed the dependency installation order, deepest dependencies are now installed first. - Improved solver error messages. - `poetry` now always reads/writes the `pyproject.toml` file with the `utf-8` encoding. - `config --list` now lists all available settings. - `init` no longer adds `pytest` to development dependencies. ### Fixed - Fixed handling of duplicate dependencies with different constraints. - Fixed system requirements in lock file for sub dependencies. - Fixed detection of new prereleases. - Fixed unsafe packages being locked. - Fixed versions detection in custom repositories. - Fixed package finding with multiple custom repositories. - Fixed handling of root incompatibilities. - Fixed an error where packages from custom repositories would not be found. - Fixed wildcard Python requirement being wrongly set in distributions metadata. - Fixed installation of packages from a custom repository. - Fixed `remove` command's case sensitivity. (Thanks to [@cauebs](https://github.com/cauebs)) - Fixed detection of `.egg-info` directory for non-poetry projects. (Thanks to [@gtors](https://github.com/gtors)) - Fixed only-wheel builds. (Thanks to [@gtors](https://github.com/gtors)) - Fixed key and array order in lock file to avoid having differences when relocking. - Fixed errors when `git` could not be found. ## [0.10.3] - 2018-06-04 ### Fixed - Fixed `self:update` command on Windows. - Fixed `self:update` not picking up new versions. - Fixed a `RuntimeError` on Python 3.7. - Fixed bad version number being picked with private repositories. - Fixed handling of duplicate dependencies with same constraint. - Fixed installation from custom repositories. - Fixed setting an explicit version in `version` command. - Fixed parsing of wildcards version constraints. ## [0.10.2] - 2018-05-31 ### Fixed - Fixed handling of `in` environment markers with commas. - Fixed a `UnicodeDecodeError` when an error occurs in venv. - Fixed Python requirements not properly set when resolving dependencies. - Fixed terminal coloring being activated even if not supported. - Fixed wrong executable being picked up on Windows in `poetry run`. - Fixed error when listing distribution links for private repositories. - Fixed handling of PEP 440 `~=` version constraint. ## [0.10.1] - 2018-05-28 ### Fixed - Fixed packages not found for prerelease version constraints when resolving dependencies. - Fixed `init` and `add` commands. ## [0.10.0] - 2018-05-28 ### Added - Added a new, more efficient dependency resolver. - Added a new `init` command to generate a `pyproject.toml` file in existing projects. - Added a new setting `settings.virtualenvs.in-project` to make `poetry` create the project's virtualenv inside the project's directory. - Added the `--extras` and `--python` options to `debug:resolve` to help debug dependency resolution. - Added a `--src` option to `new` command to create an `src` layout. - Added support for specifying the `platform` for dependencies. - Added the `--python` option to the `add` command. - Added the `--platform` option to the `add` command. - Added a `--develop` option to the install command to install path dependencies in development/editable mode. - Added a `develop` command to install the current project in development mode. ### Changed - Improved the `show` command to make it easier to check if packages are properly installed. - The `script` command has been deprecated, use `run` instead. - The `publish` command no longer build packages by default. Use `--build` to retrieve the previous behavior. - Improved support for private repositories. - Expanded version constraints now keep the original version's precision. - The lock file hash no longer uses the project's name and version. - The `LICENSE` file, or similar, is now automatically added to the built packages. ### Fixed - Fixed the dependency resolver selecting incompatible packages. - Fixed override of dependency with dependency with extras in `dev-dependencies`. ## [0.9.1] - 2018-05-18 ### Fixed - Fixed handling of package names with dots. (Thanks to [bertjwregeer](https://github.com/bertjwregeer)) - Fixed path dependencies being resolved from the current path instead of the `pyproject.toml` file. (Thanks to [radix](https://github.com/radix)) ## [0.9.0] - 2018-05-07 ### Added - Added the `cache:clear` command. - Added support for `git` dependencies in the `add` command. - Added support for `path` dependencies in the `add` command. - Added support for extras in the `add` command. - Added support for directory dependencies. - Added support for `src/` layout for packages. - Added automatic detection of `.venv` virtualenvs. ### Changed - Drastically improved dependency resolution speed. - Dependency resolution caches now use sha256 hashes. - Changed CLI error style. - Improved debugging of dependency resolution. - Poetry now attempts to find `pyproject.toml` not only in the directory it was invoked in, but in all its parents up to the root. This allows to run Poetry commands in project subdirectories. - Made the email address for authors optional. ### Fixed - Fixed handling of extras when resolving dependencies. - Fixed `self:update` command for some installation. - Fixed handling of extras when building projects. - Fixed handling of wildcard dependencies wen packaging/publishing. - Fixed an error when adding a new packages with prereleases in lock file. - Fixed packages name normalization. ## [0.8.6] - 2018-04-30 ### Fixed - Fixed config files not being created. ## [0.8.5] - 2018-04-19 ### Fixed - Fixed a bug in dependency resolution which led to installation errors. - Fixed a bug where malformed sdists would lead to dependency resolution failing. ## [0.8.4] - 2018-04-18 ### Fixed - Fixed a bug where dependencies constraints in lock were too strict. - Fixed unicode error in `search` command for Python 2.7. - Fixed error with git dependencies. ## [0.8.3] - 2018-04-16 ### Fixed - Fixed platform verification which led to missing packages. - Fixed duplicates in `pyproject.lock`. ## [0.8.2] - 2018-04-14 ### Fixed - Fixed `add` command picking up prereleases by default. - Fixed dependency resolution on Windows when unpacking distributions. - Fixed dependency resolution with post releases. - Fixed dependencies being installed even if not necessary for current system. ## [0.8.1] - 2018-04-13 ### Fixed - Fixed resolution with bad (empty) releases. - Fixed `version` for prereleases. - Fixed `search` not working outside of a project. - Fixed `self:update` not working outside of a project. ## [0.8.0] - 2018-04-13 ### Added - Added support for Python 2.7. - Added a fallback mechanism for missing dependencies. - Added the `search` command. - Added support for local files as dependencies. - Added the `self:update` command. ### Changes - Improved dependency resolution time by using cache control. ### Fixed - Fixed `install_requires` and `extras` in generated sdist. - Fixed dependency resolution crash with malformed dependencies. - Fixed errors when `license` metadata is not set. - Fixed missing information in lock file. ## [0.7.1] - 2018-04-05 ### Fixed - Fixed dependency resolution for custom repositories. ## [0.7.0] - 2018-04-04 ### Added - Added compatibility with Python 3.4 and 3.5. - Added the `version` command to automatically bump the package's version. - Added a standalone installer to install `poetry` isolated. - Added support for classifiers in `pyproject.toml`. - Added the `script` command. ### Changed - Improved dependency resolution to avoid unnecessary operations. - Improved dependency resolution speed. - Improved CLI reactivity by deferring imports. - License classifier is not automatically added to classifiers. ### Fixed - Fixed handling of markers with the `in` operator. - Fixed `update` not properly adding new packages to the lock file. - Fixed solver adding uninstall operations for non-installed packages. - Fixed `new` command creating invalid `pyproject.toml` files. ## [0.6.5] - 2018-03-22 ### Fixed - Fixed handling of extras in wheels metadata. ## [0.6.4] - 2018-03-21 ### Added - Added a `debug:info` command to get information about current environment. ### Fixed - Fixed Python version retrieval inside virtualenvs. - Fixed optional dependencies being set as required in sdist. - Fixed `--optional` option in the `add` command not being used. ## [0.6.3] - 2018-03-20 ### Fixed - Fixed built wheels not getting information from the virtualenv. - Fixed building wheel with conditional extensions. - Fixed missing files in built wheel with extensions. - Fixed call to venv binaries on windows. - Fixed subdependencies representation in lock file. ## [0.6.2] - 2018-03-19 ### Changed - Changed how wildcard constraints are handled. ### Fixed - Fixed errors with pip 9.0.2. ## [0.6.1] - 2018-02-18 ### Fixed - Fixed wheel entry points being written on a single line. - Fixed wheel metadata (Tag and Root-Is-Purelib). ## [0.6.0] - 2018-03-16 ### Added - Added support for virtualenv autogeneration (Python 3.6+ only). - Added the `run` command to execute commands inside the created virtualenvs. - Added the `debug:resolve` command to debug dependency resolution. - Added `pyproject.toml` file validation. - Added support for Markdown readme files. ### Fixed - Fixed color displayed in `show` command for semver-compatible updates. - Fixed Python requirements in publishing metadata. - Fixed `update` command reinstalling every dependency. ## [0.5.0] - 2018-03-14 ### Added - Added experimental support for package with C extensions. ### Changed - Added hashes check when installing packages. ### Fixed - Fixed handling of post releases. - Fixed python restricted dependencies not being checked against virtualenv version. - Fixed python/platform constraint not being picked up for subdependencies. - Fixed skipped packages appearing as installing. - Fixed platform specification not being used when resolving dependencies. ## [0.4.2] - 2018-03-10 ### Fixed - Fixed TypeError when `requires_dist` is null on PyPI. ## [0.4.1] - 2018-03-08 ### Fixed - Fixed missing entry point ## [0.4.0] - 2018-03-08 ### Added - Added packaging support (sdist and pure-python wheel). - Added the `build` command. - Added support for extras definition. - Added support for dependencies extras specification. - Added the `config` command. - Added the `publish` command. ### Changed - Dependencies system constraints are now respected when installing packages. - Complied with PEP 440 ### Fixed - Fixed `show` command for VCS dependencies. - Fixed handling of releases with bad markers in PyPiRepository. ## [0.3.0] - 2018-03-05 ### Added - Added `show` command. - Added the `--dry-run` option to the `add` command. ### Changed - Changed the `poetry.toml` file for the new, standardized `pyproject.toml`. - Dependencies of each package is now stored in the lock file. - Improved TOML file management. - Dependency resolver now respects the root package python version requirements. ### Fixed - Fixed the `add` command for packages with dots in their names. ## [0.2.0] - 2018-03-01 ### Added - Added `remove` command. - Added basic support for VCS (git) dependencies. - Added support for private repositories. ### Changed - Changed `poetry.lock` format. ### Fixed - Fixed dependencies solving that would lead to dependencies not being written to lock. ## [0.1.0] - 2018-02-28 Initial release [Unreleased]: https://github.com/python-poetry/poetry/compare/2.3.2...main [2.3.2]: https://github.com/python-poetry/poetry/releases/tag/2.3.2 [2.3.1]: https://github.com/python-poetry/poetry/releases/tag/2.3.1 [2.3.0]: https://github.com/python-poetry/poetry/releases/tag/2.3.0 [2.2.1]: https://github.com/python-poetry/poetry/releases/tag/2.2.1 [2.2.0]: https://github.com/python-poetry/poetry/releases/tag/2.2.0 [2.1.4]: https://github.com/python-poetry/poetry/releases/tag/2.1.4 [2.1.3]: https://github.com/python-poetry/poetry/releases/tag/2.1.3 [2.1.2]: https://github.com/python-poetry/poetry/releases/tag/2.1.2 [2.1.1]: https://github.com/python-poetry/poetry/releases/tag/2.1.1 [2.1.0]: https://github.com/python-poetry/poetry/releases/tag/2.1.0 [2.0.1]: https://github.com/python-poetry/poetry/releases/tag/2.0.1 [2.0.0]: https://github.com/python-poetry/poetry/releases/tag/2.0.0 [1.8.5]: https://github.com/python-poetry/poetry/releases/tag/1.8.5 [1.8.4]: https://github.com/python-poetry/poetry/releases/tag/1.8.4 [1.8.3]: https://github.com/python-poetry/poetry/releases/tag/1.8.3 [1.8.2]: https://github.com/python-poetry/poetry/releases/tag/1.8.2 [1.8.1]: https://github.com/python-poetry/poetry/releases/tag/1.8.1 [1.8.0]: https://github.com/python-poetry/poetry/releases/tag/1.8.0 [1.7.1]: https://github.com/python-poetry/poetry/releases/tag/1.7.1 [1.7.0]: https://github.com/python-poetry/poetry/releases/tag/1.7.0 [1.6.1]: https://github.com/python-poetry/poetry/releases/tag/1.6.1 [1.6.0]: https://github.com/python-poetry/poetry/releases/tag/1.6.0 [1.5.1]: https://github.com/python-poetry/poetry/releases/tag/1.5.1 [1.5.0]: https://github.com/python-poetry/poetry/releases/tag/1.5.0 [1.4.2]: https://github.com/python-poetry/poetry/releases/tag/1.4.2 [1.4.1]: https://github.com/python-poetry/poetry/releases/tag/1.4.1 [1.4.0]: https://github.com/python-poetry/poetry/releases/tag/1.4.0 [1.3.2]: https://github.com/python-poetry/poetry/releases/tag/1.3.2 [1.3.1]: https://github.com/python-poetry/poetry/releases/tag/1.3.1 [1.3.0]: https://github.com/python-poetry/poetry/releases/tag/1.3.0 [1.2.2]: https://github.com/python-poetry/poetry/releases/tag/1.2.2 [1.2.1]: https://github.com/python-poetry/poetry/releases/tag/1.2.1 [1.2.0]: https://github.com/python-poetry/poetry/releases/tag/1.2.0 [1.2.0rc2]: https://github.com/python-poetry/poetry/releases/tag/1.2.0rc2 [1.2.0rc1]: https://github.com/python-poetry/poetry/releases/tag/1.2.0rc1 [1.2.0b3]: https://github.com/python-poetry/poetry/releases/tag/1.2.0b3 [1.2.0b2]: https://github.com/python-poetry/poetry/releases/tag/1.2.0b2 [1.2.0b1]: https://github.com/python-poetry/poetry/releases/tag/1.2.0b1 [1.2.0a2]: https://github.com/python-poetry/poetry/releases/tag/1.2.0a2 [1.2.0a1]: https://github.com/python-poetry/poetry/releases/tag/1.2.0a1 [1.1.15]: https://github.com/python-poetry/poetry/releases/tag/1.1.15 [1.1.14]: https://github.com/python-poetry/poetry/releases/tag/1.1.14 [1.1.13]: https://github.com/python-poetry/poetry/releases/tag/1.1.13 [1.1.12]: https://github.com/python-poetry/poetry/releases/tag/1.1.12 [1.1.11]: https://github.com/python-poetry/poetry/releases/tag/1.1.11 [1.1.10]: https://github.com/python-poetry/poetry/releases/tag/1.1.10 [1.1.9]: https://github.com/python-poetry/poetry/releases/tag/1.1.9 [1.1.8]: https://github.com/python-poetry/poetry/releases/tag/1.1.8 [1.1.7]: https://github.com/python-poetry/poetry/releases/tag/1.1.7 [1.1.6]: https://github.com/python-poetry/poetry/releases/tag/1.1.6 [1.1.5]: https://github.com/python-poetry/poetry/releases/tag/1.1.5 [1.1.4]: https://github.com/python-poetry/poetry/releases/tag/1.1.4 [1.1.3]: https://github.com/python-poetry/poetry/releases/tag/1.1.3 [1.1.2]: https://github.com/python-poetry/poetry/releases/tag/1.1.2 [1.1.1]: https://github.com/python-poetry/poetry/releases/tag/1.1.1 [1.1.0]: https://github.com/python-poetry/poetry/releases/tag/1.1.0 [1.1.0rc1]: https://github.com/python-poetry/poetry/releases/tag/1.1.0rc1 [1.1.0b4]: https://github.com/python-poetry/poetry/releases/tag/1.1.0b4 [1.1.0b3]: https://github.com/python-poetry/poetry/releases/tag/1.1.0b3 [1.1.0b2]: https://github.com/python-poetry/poetry/releases/tag/1.1.0b2 [1.1.0b1]: https://github.com/python-poetry/poetry/releases/tag/1.1.0b1 [1.1.0a3]: https://github.com/python-poetry/poetry/releases/tag/1.1.0a3 [1.1.0a2]: https://github.com/python-poetry/poetry/releases/tag/1.1.0a2 [1.1.0a1]: https://github.com/python-poetry/poetry/releases/tag/1.1.0a1 [1.0.10]: https://github.com/python-poetry/poetry/releases/tag/1.0.10 [1.0.9]: https://github.com/python-poetry/poetry/releases/tag/1.0.9 [1.0.8]: https://github.com/python-poetry/poetry/releases/tag/1.0.8 [1.0.7]: https://github.com/python-poetry/poetry/releases/tag/1.0.7 [1.0.6]: https://github.com/python-poetry/poetry/releases/tag/1.0.6 [1.0.5]: https://github.com/python-poetry/poetry/releases/tag/1.0.5 [1.0.4]: https://github.com/python-poetry/poetry/releases/tag/1.0.4 [1.0.3]: https://github.com/python-poetry/poetry/releases/tag/1.0.3 [1.0.2]: https://github.com/python-poetry/poetry/releases/tag/1.0.2 [1.0.1]: https://github.com/python-poetry/poetry/releases/tag/1.0.1 [1.0.0]: https://github.com/python-poetry/poetry/releases/tag/1.0.0 [0.12.17]: https://github.com/python-poetry/poetry/releases/tag/0.12.17 [0.12.16]: https://github.com/python-poetry/poetry/releases/tag/0.12.16 [0.12.15]: https://github.com/python-poetry/poetry/releases/tag/0.12.15 [0.12.14]: https://github.com/python-poetry/poetry/releases/tag/0.12.14 [0.12.13]: https://github.com/python-poetry/poetry/releases/tag/0.12.13 [0.12.12]: https://github.com/python-poetry/poetry/releases/tag/0.12.12 [0.12.11]: https://github.com/python-poetry/poetry/releases/tag/0.12.11 [0.12.10]: https://github.com/python-poetry/poetry/releases/tag/0.12.10 [0.12.9]: https://github.com/python-poetry/poetry/releases/tag/0.12.9 [0.12.8]: https://github.com/python-poetry/poetry/releases/tag/0.12.8 [0.12.7]: https://github.com/python-poetry/poetry/releases/tag/0.12.7 [0.12.6]: https://github.com/python-poetry/poetry/releases/tag/0.12.6 [0.12.5]: https://github.com/python-poetry/poetry/releases/tag/0.12.5 [0.12.4]: https://github.com/python-poetry/poetry/releases/tag/0.12.4 [0.12.3]: https://github.com/python-poetry/poetry/releases/tag/0.12.3 [0.12.2]: https://github.com/python-poetry/poetry/releases/tag/0.12.2 [0.12.1]: https://github.com/python-poetry/poetry/releases/tag/0.12.1 [0.12.0]: https://github.com/python-poetry/poetry/releases/tag/0.12.0 [0.11.5]: https://github.com/python-poetry/poetry/releases/tag/0.11.5 [0.11.4]: https://github.com/python-poetry/poetry/releases/tag/0.11.4 [0.11.3]: https://github.com/python-poetry/poetry/releases/tag/0.11.3 [0.11.2]: https://github.com/python-poetry/poetry/releases/tag/0.11.2 [0.11.1]: https://github.com/python-poetry/poetry/releases/tag/0.11.1 [0.11.0]: https://github.com/python-poetry/poetry/releases/tag/0.11.0 [0.10.3]: https://github.com/python-poetry/poetry/releases/tag/0.10.3 [0.10.2]: https://github.com/python-poetry/poetry/releases/tag/0.10.2 [0.10.1]: https://github.com/python-poetry/poetry/releases/tag/0.10.1 [0.10.0]: https://github.com/python-poetry/poetry/releases/tag/0.10.0 [0.9.1]: https://github.com/python-poetry/poetry/releases/tag/0.9.1 [0.9.0]: https://github.com/python-poetry/poetry/releases/tag/0.9.0 [0.8.6]: https://github.com/python-poetry/poetry/releases/tag/0.8.6 [0.8.5]: https://github.com/python-poetry/poetry/releases/tag/0.8.5 [0.8.4]: https://github.com/python-poetry/poetry/releases/tag/0.8.4 [0.8.3]: https://github.com/python-poetry/poetry/releases/tag/0.8.3 [0.8.2]: https://github.com/python-poetry/poetry/releases/tag/0.8.2 [0.8.1]: https://github.com/python-poetry/poetry/releases/tag/0.8.1 [0.8.0]: https://github.com/python-poetry/poetry/releases/tag/0.8.0 [0.7.1]: https://github.com/python-poetry/poetry/releases/tag/0.7.1 [0.7.0]: https://github.com/python-poetry/poetry/releases/tag/0.7.0 [0.6.5]: https://github.com/python-poetry/poetry/releases/tag/0.6.5 [0.6.4]: https://github.com/python-poetry/poetry/releases/tag/0.6.4 [0.6.3]: https://github.com/python-poetry/poetry/releases/tag/0.6.3 [0.6.2]: https://github.com/python-poetry/poetry/releases/tag/0.6.2 [0.6.1]: https://github.com/python-poetry/poetry/releases/tag/0.6.1 [0.6.0]: https://github.com/python-poetry/poetry/releases/tag/0.6.0 [0.5.0]: https://github.com/python-poetry/poetry/releases/tag/0.5.0 [0.4.2]: https://github.com/python-poetry/poetry/releases/tag/0.4.2 [0.4.1]: https://github.com/python-poetry/poetry/releases/tag/0.4.1 [0.4.0]: https://github.com/python-poetry/poetry/releases/tag/0.4.0 [0.3.0]: https://github.com/python-poetry/poetry/releases/tag/0.3.0 [0.2.0]: https://github.com/python-poetry/poetry/releases/tag/0.2.0 [0.1.0]: https://github.com/python-poetry/poetry/releases/tag/0.1.0 ================================================ FILE: CITATION.cff ================================================ cff-version: 1.2.0 title: "Poetry: Python packaging and dependency management made easy" message: >- If you use this software, please cite it using the metadata from this file. authors: - family-names: Eustace given-names: Sébastien - name: "The Poetry contributors" abstract: >- Poetry helps you declare, manage and install dependencies of Python projects, ensuring you have the right stack everywhere. Poetry replaces setup.py, requirements.txt, setup.cfg, MANIFEST.in and Pipfile with a simple pyproject.toml based project format. license: MIT license-url: "https://github.com/python-poetry/poetry/blob/main/LICENSE" repository-code: "https://github.com/python-poetry/poetry" keywords: - python - packaging - dependency management type: software url: "https://python-poetry.org" ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sebastien@eustace.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE ================================================ Copyright (c) 2018-present Sébastien Eustace Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Poetry: Python packaging and dependency management made easy [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) [![Stable Version](https://img.shields.io/pypi/v/poetry?label=stable)][PyPI Releases] [![Pre-release Version](https://img.shields.io/github/v/release/python-poetry/poetry?label=pre-release&include_prereleases&sort=semver)][PyPI Releases] [![Python Versions](https://img.shields.io/pypi/pyversions/poetry)][PyPI] [![Download Stats](https://img.shields.io/pypi/dm/poetry)](https://pypistats.org/packages/poetry) [![Discord](https://img.shields.io/discord/487711540787675139?logo=discord)][Discord] Poetry helps you declare, manage and install dependencies of Python projects, ensuring you have the right stack everywhere. ![Poetry Install](https://raw.githubusercontent.com/python-poetry/poetry/main/assets/install.gif) Poetry replaces `setup.py`, `requirements.txt`, `setup.cfg`, `MANIFEST.in` and `Pipfile` with a simple `pyproject.toml` based project format. ```toml [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [project] name = "my-package" version = "0.1.0" description = "The description of the package" readme = "README.md" license = "MIT" license-files = ["LICENSE"] # No Python upper bound for package metadata requires-python = ">=3.9" authors = [ { name = "Sébastien Eustace", email = "sebastien@eustace.io" }, ] # Keywords (translated to tags on the package index) keywords = ["packaging", "poetry"] dependencies = [ # equivalent to ^3.8.1 with semver constraints "aiohttp (>=3.8.1,<4.0.0)", # dependency with extras "requests[security] (>=2.28,<3.0)", # version-specific dependency with prereleases allowed (see below) "tomli (>=2.0.1,<3.0.0) ; python_version < '3.11'", # git dependency with branch specified "cleo @ git+https://github.com/python-poetry/cleo.git@main", ] [project.urls] repository = "https://github.com/python-poetry/poetry" homepage = "https://python-poetry.org" # Scripts are easily expressed [project.scripts] my_package_cli = "my_package.console:run" [project.optional-dependencies] # optional dependency to be installed via 'poetry install -E my-extra' my-extra = ["pendulum (>=3.1.0,<4.0.0)"] [tool.poetry.dependencies] # Python upper bound for locking python = ">=3.9,<4.0" # Version-specific dependencies with prereleases allowed tomli = { allow-prereleases = true } # Dependency groups are supported for organizing your dependencies [dependency-groups] dev = ["pytest (>=7.1.2,<8.0.0)", "pytest-cov (>=3.0,<4.0)"] docs = ["Sphinx (>=5.1.1,<6.0.0)"] # ...and can be installed only when explicitly requested # via 'poetry install --with docs' [tool.poetry.group.docs] optional = true # Alternatively, you can use Poetry specific syntax # to specify dependency groups [tool.poetry.group.lint] optional = true [tool.poetry.group.lint.dependencies] ruff = ">=0.10.0" ``` ## Installation Poetry supports multiple installation methods, including a simple script found at [install.python-poetry.org]. For full installation instructions, including advanced usage of the script, alternate install methods, and CI best practices, see the full [installation documentation]. ## Documentation [Documentation] for the current version of Poetry (as well as the development branch and recently out of support versions) is available from the [official website]. ## Contribute Poetry is a large, complex project always in need of contributors. For those new to the project, a list of [suggested issues] to work on in Poetry and poetry-core is available. The full [contributing documentation] also provides helpful guidance. ## Resources * [Releases][PyPI Releases] * [Official Website] * [Documentation] * [Issue Tracker] * [Discord] [PyPI]: https://pypi.org/project/poetry/ [PyPI Releases]: https://pypi.org/project/poetry/#history [Official Website]: https://python-poetry.org [Documentation]: https://python-poetry.org/docs/ [Issue Tracker]: https://github.com/python-poetry/poetry/issues [Suggested Issues]: https://github.com/python-poetry/poetry/contribute [Contributing Documentation]: https://python-poetry.org/docs/contributing [Discord]: https://discord.com/invite/awxPgve [install.python-poetry.org]: https://install.python-poetry.org [Installation Documentation]: https://python-poetry.org/docs/#installation ## Related Projects * [poetry-core](https://github.com/python-poetry/poetry-core): PEP 517 build-system for Poetry projects, and dependency-free core functionality of the Poetry frontend * [poetry-plugin-export](https://github.com/python-poetry/poetry-plugin-export): Export Poetry projects/lock files to foreign formats like requirements.txt * [poetry-plugin-bundle](https://github.com/python-poetry/poetry-plugin-bundle): Install Poetry projects/lock files to external formats like virtual environments * [install.python-poetry.org](https://github.com/python-poetry/install.python-poetry.org): The official Poetry installation script * [website](https://github.com/python-poetry/website): The official Poetry website and blog ## Supporters Thanks to [JetBrains](https://www.jetbrains.com) for supporting us with licenses for their tools. [JetBrains logo.](https://www.jetbrains.com) ================================================ FILE: docs/_index.md ================================================ --- title: "Introduction" draft: false type: docs layout: "single" menu: docs: weight: 0 --- # Introduction Poetry is a tool for **dependency management** and **packaging** in Python. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you. Poetry offers a lockfile to ensure repeatable installs, and can build your project for distribution. ## System requirements Poetry requires **Python 3.10+**. It is multi-platform and the goal is to make it work equally well on Linux, macOS and Windows. ## Installation {{% note %}} If you are viewing documentation for the development branch, you may wish to install a preview or development version of Poetry. See the **advanced** installation instructions to use a preview or alternate version of Poetry. {{% /note %}} {{< tabs tabTotal="4" tabID1="installing-with-pipx" tabID2="installing-with-the-official-installer" tabID3="installing-manually" tabID4="ci-recommendations" tabName1="With pipx" tabName2="With the official installer" tabName3="Manually (advanced)" tabName4="CI recommendations">}} {{< tab tabID="installing-with-pipx" >}} [`pipx`](https://github.com/pypa/pipx) is used to install Python CLI applications globally while still isolating them in virtual environments. `pipx` will manage upgrades and uninstalls when used to install Poetry. {{< steps >}} {{< step >}} **Install pipx** If `pipx` is not already installed, you can follow any of the options in the [official pipx installation instructions](https://pipx.pypa.io/stable/installation/). Any non-ancient version of `pipx` will do. {{< /step >}} {{< step >}} **Install Poetry** ```bash pipx install poetry ``` {{< /step >}} {{< step >}} **Install Poetry (advanced)** {{% note %}} You can skip this step, if you simply want the latest version and already installed Poetry as described in the previous step. This step details advanced usages of this installation method. For example, installing Poetry from source, having multiple versions installed at the same time etc. {{% /note %}} `pipx` can install different versions of Poetry, using the same syntax as pip: ```bash pipx install poetry==1.8.4 ``` `pipx` can also install versions of Poetry in parallel, which allows for easy testing of alternate or prerelease versions. Each version is given a unique, user-specified suffix, which will be used to create a unique binary name: ```bash pipx install --suffix=@1.8.4 poetry==1.8.4 poetry@1.8.4 --version ``` ```bash pipx install --suffix=@preview --pip-args=--pre poetry poetry@preview --version ``` Finally, `pipx` can install any valid [pip requirement spec](https://pip.pypa.io/en/stable/cli/pip_install/), which allows for installations of the development version from `git`, or even for local testing of pull requests: ```bash pipx install --suffix @main git+https://github.com/python-poetry/poetry.git@main pipx install --suffix @pr1234 git+https://github.com/python-poetry/poetry.git@refs/pull/1234/head ``` {{< /step >}} {{< step >}} **Update Poetry** ```bash pipx upgrade poetry ``` {{< /step >}} {{< step >}} **Uninstall Poetry** ```bash pipx uninstall poetry ``` {{< /step >}} {{< /steps >}} {{< /tab >}} {{< tab tabID="installing-with-the-official-installer" >}} We provide a custom installer that will install Poetry in a new virtual environment and allows Poetry to manage its own environment. {{< steps >}} {{< step >}} **Install Poetry** The installer script is available directly at [install.python-poetry.org](https://install.python-poetry.org), and is developed in [its own repository](https://github.com/python-poetry/install.python-poetry.org). The script can be executed directly (i.e. 'curl python') or downloaded and then executed from disk (e.g. in a CI environment). **Linux, macOS, Windows (WSL)** ```bash curl -sSL https://install.python-poetry.org | python3 - ``` **Windows (Powershell)** ```powershell (Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | py - ``` {{% note %}} If you have installed Python through the Microsoft Store, replace `py` with `python` in the command above. {{% /note %}} {{< /step >}} {{< step >}} **Install Poetry (advanced)** {{% note %}} You can skip this step, if you simply want the latest version and already installed Poetry as described in the previous step. This step details advanced usages of this installation method. For example, installing Poetry from source, using a pre-release build, configuring a different installation location etc. {{% /note %}} By default, Poetry is installed into a platform and user-specific directory: - `~/Library/Application Support/pypoetry` on macOS. - `~/.local/share/pypoetry` on Linux/Unix. - `%APPDATA%\pypoetry` on Windows. If you wish to change this, you may define the `$POETRY_HOME` environment variable: ```bash curl -sSL https://install.python-poetry.org | POETRY_HOME=/etc/poetry python3 - ``` If you want to install prerelease versions, you can do so by passing the `--preview` option to the installation script or by using the `$POETRY_PREVIEW` environment variable: ```bash curl -sSL https://install.python-poetry.org | python3 - --preview curl -sSL https://install.python-poetry.org | POETRY_PREVIEW=1 python3 - ``` Similarly, if you want to install a specific version, you can use `--version` option or the `$POETRY_VERSION` environment variable: ```bash curl -sSL https://install.python-poetry.org | python3 - --version 1.8.4 curl -sSL https://install.python-poetry.org | POETRY_VERSION=1.8.4 python3 - ``` You can also install Poetry from a `git` repository by using the `--git` option: ```bash curl -sSL https://install.python-poetry.org | python3 - --git https://github.com/python-poetry/poetry.git@main ```` If you want to install different versions of Poetry in parallel, a good approach is the installation with pipx and suffix. {{< /step >}} {{< step >}} **Add Poetry to your PATH** The installer creates a `poetry` wrapper in a well-known, platform-specific directory: - `$HOME/.local/bin` on Unix. - `%APPDATA%\Python\Scripts` on Windows. - `$POETRY_HOME/bin` if `$POETRY_HOME` is set. {{% note %}} If you have installed Python through the Microsoft Store, the `poetry` binary will be installed to a different location, for example: ``` %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0 \LocalCache\Roaming\Python\Scripts ``` Replace `3.12` with your installed Python version and `qbz5n2kfra8p0` with your suffix. {{% /note %}} If this directory is not present in your `$PATH`, you can add it in order to invoke Poetry as `poetry`. Alternatively, the full path to the `poetry` binary can always be used: - `~/Library/Application Support/pypoetry/venv/bin/poetry` on macOS. - `~/.local/share/pypoetry/venv/bin/poetry` on Linux/Unix. - `%APPDATA%\pypoetry\venv\Scripts\poetry` on Windows. - `$POETRY_HOME/venv/bin/poetry` if `$POETRY_HOME` is set. {{< /step >}} {{< step >}} **Use Poetry** Once Poetry is installed and in your `$PATH`, you can execute the following: ```bash poetry --version ``` If you see something like `Poetry (version 2.0.0)`, your installation is ready to use! {{< /step >}} {{< step >}} **Update Poetry** Poetry is able to update itself when installed using the official installer. {{% warning %}} Especially on Windows, `self update` may be problematic so that a re-install with the installer should be preferred. {{% /warning %}} ```bash poetry self update ``` If you want to install pre-release versions, you can use the `--preview` option. ```bash poetry self update --preview ``` And finally, if you want to install a specific version, you can pass it as an argument to `self update`. ```bash poetry self update 1.8.4 ``` {{< /step >}} {{< step >}} **Uninstall Poetry** If you decide Poetry isn't your thing, you can completely remove it from your system by running the installer again with the `--uninstall` option or by setting the `POETRY_UNINSTALL` environment variable before executing the installer. ```bash curl -sSL https://install.python-poetry.org | python3 - --uninstall curl -sSL https://install.python-poetry.org | POETRY_UNINSTALL=1 python3 - ``` {{< /step >}} {{< /steps >}} {{< /tab >}} {{< tab tabID="installing-manually" >}} Poetry can be installed manually using `pip` and the `venv` module. By doing so you will essentially perform the steps carried out by the official installer. As this is an advanced installation method, these instructions are Unix-only and omit specific examples such as installing from `git`. The variable `$VENV_PATH` will be used to indicate the path at which the virtual environment was created. ```bash python3 -m venv $VENV_PATH $VENV_PATH/bin/pip install -U pip setuptools $VENV_PATH/bin/pip install poetry ``` Poetry will be available at `$VENV_PATH/bin/poetry` and can be invoked directly or symlinked elsewhere. To uninstall Poetry, simply delete the entire `$VENV_PATH` directory. {{< /tab >}} {{< tab tabID="ci-recommendations" >}} Unlike development environments, where making use of the latest tools is desirable, in a CI environment reproducibility should be made the priority. Here are some suggestions for installing Poetry in such an environment. **Version pinning** Whatever method you use, it is highly recommended to explicitly control the version of Poetry used, so that you are able to upgrade after performing your own validation. Each install method has a different syntax for setting the version that is used in the following examples. **Using pipx** Just as `pipx` is a powerful tool for development use, it is equally useful in a CI environment and should be one of your top choices for use of Poetry in CI. ```bash pipx install poetry==2.0.0 ``` **Using install.python-poetry.org** {{% note %}} The official installer script ([install.python-poetry.org](https://install.python-poetry.org)) offers a streamlined and simplified installation of Poetry, sufficient for developer use or for simple pipelines. However, in a CI environment the other two supported installation methods (pipx and manual) should be seriously considered. {{% /note %}} Downloading a copy of the installer script to a place accessible by your CI pipelines (or maintaining a copy of the [repository](https://github.com/python-poetry/install.python-poetry.org)) is strongly suggested, to ensure your pipeline's stability and to maintain control over what code is executed. By default, the installer will install to a user-specific directory. In more complex pipelines that may make accessing Poetry difficult (especially in cases like multi-stage container builds). It is highly suggested to make use of `$POETRY_HOME` when using the official installer in CI, as that way the exact paths can be controlled. ```bash export POETRY_HOME=/opt/poetry python3 install-poetry.py --version 2.0.0 $POETRY_HOME/bin/poetry --version ``` **Using pip (aka manually)** For maximum control in your CI environment, installation with `pip` is fully supported and something you should consider. While this requires more explicit commands and knowledge of Python packaging from you, it in return offers the best debugging experience, and leaves you subject to the fewest external tools. ```bash export POETRY_HOME=/opt/poetry python3 -m venv $POETRY_HOME $POETRY_HOME/bin/pip install poetry==2.0.0 $POETRY_HOME/bin/poetry --version ``` {{% note %}} If you install Poetry via `pip`, ensure you have Poetry installed into an isolated environment that is **not the same** as the target environment managed by Poetry. If Poetry and your project are installed into the same environment, Poetry is likely to upgrade or uninstall its own dependencies (causing hard-to-debug and understand errors). {{% /note %}} {{< /tab >}} {{< /tabs >}} {{% warning %}} Poetry should always be installed in a dedicated virtual environment to isolate it from the rest of your system. Each of the above described installation methods ensures that. It should in no case be installed in the environment of the project that is to be managed by Poetry. This ensures that Poetry's own dependencies will not be accidentally upgraded or uninstalled. In addition, the isolated virtual environment in which poetry is installed should not be activated for running poetry commands. {{% /warning %}} ## Enable tab completion for Bash, Fish, or Zsh `poetry` supports generating completion scripts for Bash, Fish, and Zsh. {{% note %}} You may need to restart your shell in order for these changes to take effect. {{% /note %}} See `poetry help completions` for full details, but the gist is as simple as using one of the following: ### Bash #### Auto-loaded (recommended) ```bash poetry completions bash >> ~/.bash_completion ``` #### Lazy-loaded ```bash poetry completions bash > ${XDG_DATA_HOME:-~/.local/share}/bash-completion/completions/poetry ``` ### Fish ```fish poetry completions fish > ~/.config/fish/completions/poetry.fish ``` ### Zsh ```zsh poetry completions zsh > ~/.zfunc/_poetry ``` You must then add the following lines in your `~/.zshrc`, if they do not already exist: ```bash fpath+=~/.zfunc autoload -Uz compinit && compinit ``` #### Oh My Zsh ```zsh mkdir $ZSH_CUSTOM/plugins/poetry poetry completions zsh > $ZSH_CUSTOM/plugins/poetry/_poetry ``` You must then add `poetry` to your plugins array in `~/.zshrc`: ```text plugins( poetry ... ) ``` #### Prezto ```zsh poetry completions zsh > ~/.zprezto/modules/completion/external/src/_poetry ``` If completions still don't work, try removing `~/.cache/prezto/zcompcache` and starting a new shell. ================================================ FILE: docs/basic-usage.md ================================================ --- title: "Basic usage" draft: false type: docs layout: single menu: docs: weight: 10 --- # Basic usage For the basic usage introduction we will be installing `pendulum`, a datetime library. If you have not yet installed Poetry, refer to the [Introduction]({{< relref "docs" >}} "Introduction") chapter. ## Project setup First, let's create our new project, let's call it `poetry-demo`: ```bash poetry new poetry-demo ``` This will create the `poetry-demo` directory with the following content: ```text poetry-demo ├── pyproject.toml ├── README.md ├── src │ └── poetry_demo │ └── __init__.py └── tests └── __init__.py ``` The `pyproject.toml` file is what is the most important here. This will orchestrate your project and its dependencies. For now, it looks like this: ```toml [project] name = "poetry-demo" version = "0.1.0" description = "" authors = [ {name = "Sébastien Eustace", email = "sebastien@eustace.io"} ] readme = "README.md" requires-python = ">=3.9" dependencies = [ ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" ``` Poetry assumes your package contains a package with the same name as `project.name` located in the root of your project. If this is not the case, populate [`tool.poetry.packages`]({{< relref "pyproject#packages" >}}) to specify your packages and their locations. Similarly, the traditional `MANIFEST.in` file is replaced by the `project.readme`, `tool.poetry.include`, and `tool.poetry.exclude` sections. `tool.poetry.exclude` is additionally implicitly populated by your `.gitignore`. For full documentation on the project format, see the [pyproject section]({{< relref "pyproject" >}}) of the documentation. ### Setting a Python Version {{% note %}} Unlike with other packages, Poetry will not automatically install a python interpreter for you. If you want to run Python files in your package like a script or application, you must _bring your own_ python interpreter to run them. {{% /note %}} Poetry will require you to explicitly specify what versions of Python you intend to support, and its universal locking will guarantee that your project is installable (and all dependencies claim support for) all supported Python versions. Again, it's important to remember that -- unlike other dependencies -- setting a Python version is merely specifying which versions of Python you intend to support. For example, in this `pyproject.toml` file: ```toml [project] requires-python = ">=3.9" ``` we are allowing any version of Python 3 that is greater or equal than `3.9.0`. When you run `poetry install`, you must have access to some version of a Python interpreter that satisfies this constraint available on your system. Poetry will not install a Python interpreter for you. ### Initialising a pre-existing project Instead of creating a new project, Poetry can be used to 'initialize' a pre-populated directory. To interactively create a `pyproject.toml` file in directory `pre-existing-project`: ```bash cd pre-existing-project poetry init ``` ### Operating modes Poetry can be operated in two different modes. The default mode is the **package mode**, which is the right mode if you want to package your project into an sdist or a wheel and perhaps publish it to a package index. In this mode, some metadata such as `name` and `version`, which are required for packaging, are mandatory. Further, the project itself will be installed in editable mode when running `poetry install`. If you want to use Poetry only for dependency management but not for packaging, you can use the **non-package mode**: ```toml [tool.poetry] package-mode = false ``` In this mode, metadata such as `name` and `version` are optional. Therefore, it is not possible to build a distribution or publish the project to a package index. Further, when running `poetry install`, Poetry does not try to install the project itself, but only its dependencies (same as `poetry install --no-root`). {{% note %}} In the [pyproject section]({{< relref "pyproject" >}}) you can see which fields are required in package mode. {{% /note %}} ### Specifying dependencies If you want to add dependencies to your project, you can specify them in the `project` section. ```toml [project] # ... dependencies = [ "pendulum (>=2.1,<3.0)" ] ``` As you can see, it takes a mapping of **package names** and **version constraints**. Poetry uses this information to search for the right set of files in package "repositories" that you register in the `tool.poetry.source` section, or on [PyPI](https://pypi.org) by default. Also, instead of modifying the `pyproject.toml` file by hand, you can use the `add` command. ```bash $ poetry add pendulum ``` It will automatically find a suitable version constraint **and install** the package and sub-dependencies. Poetry supports a rich [dependency specification]({{< relref "dependency-specification" >}}) syntax, including caret, tilde, wildcard, inequality and [multiple constraints]({{< relref "dependency-specification#multiple-constraints-dependencies" >}}) requirements. ## Using your virtual environment By default, Poetry creates a virtual environment in `{cache-dir}/virtualenvs`. You can change the [`cache-dir`]({{< relref "configuration#cache-dir" >}} "cache-dir configuration documentation") value by editing the Poetry configuration. Additionally, you can use the [`virtualenvs.in-project`]({{< relref "configuration#virtualenvsin-project" >}}) configuration variable to create virtual environments within your project directory. There are several ways to run commands within this virtual environment. {{% note %}} **External virtual environment management** Poetry will detect and respect an existing virtual environment that has been externally activated. This is a powerful mechanism that is intended to be an alternative to Poetry's built-in, simplified environment management. To take advantage of this, simply activate a virtual environment using your preferred method or tooling, before running any Poetry commands that expect to manipulate an environment. {{% /note %}} ### Using `poetry run` To run your script simply use `poetry run python your_script.py`. Likewise if you have command line tools such as `pytest` or `black` you can run them using `poetry run pytest`. {{% note %}} If managing your own virtual environment externally, you do not need to use `poetry run` since you will, presumably, already have activated that virtual environment and made available the correct python instance. For example, these commands should output the same python path: ```shell conda activate your_env_name which python poetry run which python eval "$(poetry env activate)" which python ``` {{% /note %}} ### Activating the virtual environment See [Activating the virtual environment]({{< relref "managing-environments#activating-the-environment" >}}). ## Version constraints In our example, we are requesting the `pendulum` package with the version constraint `>=2.1.0 <3.0.0`. This means any version greater or equal to 2.1.0 and less than 3.0.0. Please read [Dependency specification]({{< relref "dependency-specification" >}} "Dependency specification documentation") for more in-depth information on versions, how versions relate to each other, and on the different ways you can specify dependencies. {{% note %}} **How does Poetry download the right files?** When you specify a dependency in `pyproject.toml`, Poetry first takes the name of the package that you have requested and searches for it in any repository you have registered using the `repositories` key. If you have not registered any extra repositories, or it does not find a package with that name in the repositories you have specified, it falls back to PyPI. When Poetry finds the right package, it then attempts to find the best match for the version constraint you have specified. {{% /note %}} ## Installing dependencies To install the defined dependencies for your project, just run the [`install`]({{< relref "cli#install" >}}) command. ```bash poetry install ``` When you run this command, one of two things may happen: ### Installing without `poetry.lock` If you have never run the command before and there is also no `poetry.lock` file present, Poetry simply resolves all dependencies listed in your `pyproject.toml` file and downloads the latest version of their files. When Poetry has finished installing, it writes all the packages and their exact versions that it downloaded to the `poetry.lock` file, locking the project to those specific versions. You should commit the `poetry.lock` file to your project repo so that all people working on the project are locked to the same versions of dependencies (more below). ### Installing with `poetry.lock` This brings us to the second scenario. If there is already a `poetry.lock` file as well as a `pyproject.toml` file when you run `poetry install`, it means either you ran the `install` command before, or someone else on the project ran the `install` command and committed the `poetry.lock` file to the project (which is good). Either way, running `install` when a `poetry.lock` file is present resolves and installs all dependencies that you listed in `pyproject.toml`, but Poetry uses the exact versions listed in `poetry.lock` to ensure that the package versions are consistent for everyone working on your project. As a result you will have all dependencies requested by your `pyproject.toml` file, but they may not all be at the very latest available versions (some dependencies listed in the `poetry.lock` file may have released newer versions since the file was created). This is by design, it ensures that your project does not break because of unexpected changes in dependencies. ### Committing your `poetry.lock` file to version control #### As an application developer Application developers commit `poetry.lock` to get more reproducible builds. Committing this file to VC is important because it will cause anyone who sets up the project to use the exact same versions of the dependencies that you are using. Your CI server, production machines, other developers in your team, everything and everyone runs on the same dependencies, which mitigates the potential for bugs affecting only some parts of the deployments. Even if you develop alone, in six months when reinstalling the project you can feel confident the dependencies installed are still working even if your dependencies released many new versions since then. (See note below about using the update command.) {{% warning %}} If you have added the recommended [`[build-system]`]({{< relref "pyproject#poetry-and-pep-517" >}}) section to your project's pyproject.toml then you _can_ successfully install your project and its dependencies into a virtual environment using a command like `pip install -e .`. However, pip will not use the lock file to determine dependency versions as the poetry-core build system is intended for library developers (see next section). {{% /warning %}} #### As a library developer Library developers have more to consider. Your users are application developers, and your library will run in a Python environment you don't control. The application ignores your library's lock file. It can use whatever dependency version meets the constraints in your `pyproject.toml`. The application will probably use the latest compatible dependency version. If your library's `poetry.lock` falls behind some new dependency version that breaks things for your users, you're likely to be the last to find out about it. A simple way to avoid such a scenario is to omit the `poetry.lock` file. However, by doing so, you sacrifice reproducibility and performance to a certain extent. Without a lockfile, it can be difficult to find the reason for failing tests, because in addition to obvious code changes an unnoticed library update might be the culprit. Further, Poetry will have to lock before installing a dependency if `poetry.lock` has been omitted. Depending on the number of dependencies, locking may take a significant amount of time. If you do not want to give up the reproducibility and performance benefits, consider a regular refresh of `poetry.lock` to stay up-to-date and reduce the risk of sudden breakage for users. ### Installing dependencies only The current project is installed in [editable](https://pip.pypa.io/en/stable/topics/local-project-installs/) mode by default. If you want to install the dependencies only, run the `install` command with the `--no-root` flag: ```bash poetry install --no-root ``` ## Updating dependencies to their latest versions As mentioned above, the `poetry.lock` file prevents you from automatically getting the latest versions of your dependencies. To update to the latest versions, use the `update` command. This will fetch the latest matching versions (according to your `pyproject.toml` file) and update the lock file with the new versions. (This is equivalent to deleting the `poetry.lock` file and running `install` again.) {{% note %}} Poetry will display a **Warning** when executing an install command if `poetry.lock` and `pyproject.toml` are not synchronized. {{% /note %}} ================================================ FILE: docs/building-extension-modules.md ================================================ --- title: "Building extension modules" draft: false type: docs layout: single menu: docs: weight: 125 --- # Building Extension Modules {{% warning %}} While this feature has been around since almost the beginning of the Poetry project and has needed minimal changes, it is still considered unstable. You can participate in the discussions about stabilizing this feature [here](https://github.com/python-poetry/poetry/issues/2740). And as always, your contributions towards the goal of improving this feature are also welcome. {{% /warning %}} Poetry allows a project developer to introduce support for, build and distribute native extensions within their project. In order to achieve this, at the highest level, the following steps are required. {{< steps >}} {{< step >}} **Add Build Dependencies** The build dependencies, in this context, refer to those Python packages that are required in order to successfully execute your build script. Common examples include `cython`, `meson`, `maturin`, `setuptools` etc., depending on how your extension is built. {{% note %}} You must assume that only Python built-ins are available by default in a build environment. This means, if you need even packages like `setuptools`, it must be explicitly declared. {{% /note %}} The necessary build dependencies must be added to the `build-system.requires` section of your `pyproject.toml` file. ```toml [build-system] requires = ["poetry-core", "setuptools", "cython"] build-backend = "poetry.core.masonry.api" ``` {{% note %}} It is recommended that you consider specifying version constraints to all entries in `build-system.requires` in order to avoid surprises if one of the packages introduce a breaking change. For example, you can set `cython` to `cython>=3.0.11,<4.0.0` to ensure no major version upgrades are used. {{% /note %}} {{% note %}} If you wish to develop the build script within your project's virtual environment, then you must also add the dependencies to your project explicitly to a dependency group - the name of which is not important. ```sh poetry add --group=build setuptools cython ``` {{% /note %}} {{< /step >}} {{< step >}} **Add Build Script** The build script can be a free-form Python script that uses any dependency specified in the previous step. This can be named as needed, but **must** be located within the project root directory (or a subdirectory) and also **must** be included in your source distribution. You can see the [example snippets section]({{< relref "#example-snippets" >}}) for inspiration. {{% note %}} The build script is always executed from the project root. And it is expected to move files around to their destinations as expected by Poetry as per your `pyproject.toml` file. {{% /note %}} ```toml [tool.poetry.build] script = "relative/path/to/build-extension.py" ``` {{% note %}} The name of the build script is arbitrary. Common practice has been to name it `build.py`, however, this is not mandatory. You **should** consider [using a subdirectory]({{< relref "#can-i-store-the-build-script-in-a-subdirectory" >}}) if feasible. {{% /note %}} {{< /step >}} {{< step >}} **Specify Distribution Files** {{% warning %}} The following is an example, and should not be considered as complete. {{% /warning %}} ```toml [tool.poetry] ... include = [ { path = "package/**/*.so", format = "wheel" }, # sources must be present in sdist, can be ignored if you only have *.pyx sources { path = "package/**/*.c", format = "sdist" }, ] ``` The key takeaway here should be the following. You can refer to the [`pyproject.toml`]({{< relref "pyproject#exclude-and-include" >}}) documentation for information on each of the relevant sections. 1. Include your build outputs in your wheel. 2. Exclude your build inputs from your wheel. 3. Include your build inputs to your source distribution. {{< /step >}} {{< /steps >}} ## Example Snippets ### Cython {{< tabs tabTotal="3" tabID1="cython-pyproject" tabName1="pyproject.toml" tabID2="cython-build-script" tabName2="build-extension.py" tabID3="cython-src-tree" tabName3="Source Tree">}} {{< tab tabID="cython-pyproject" >}} ```toml [build-system] requires = ["poetry-core", "cython", "setuptools"] build-backend = "poetry.core.masonry.api" [tool.poetry] ... packages = [ { include = "package", from = "src"}, ] include = [ { path = "src/package/**/*.so", format = "wheel" }, ] # if not already excluded via .gitignore exclude = [ "**/*.c" ] [tool.poetry.build] script = "scripts/build-extension.py" ``` {{< /tab >}} {{< tab tabID="cython-build-script" >}} ```py from __future__ import annotations import os import shutil from pathlib import Path from Cython.Build import cythonize from setuptools import Distribution from setuptools import Extension from setuptools.command.build_ext import build_ext COMPILE_ARGS = ["-march=native", "-O3", "-msse", "-msse2", "-mfma", "-mfpmath=sse"] LINK_ARGS = [] INCLUDE_DIRS = [] LIBRARIES = ["m"] def build() -> None: extensions = [ Extension( "*", ["src/package/*.pyx"], extra_compile_args=COMPILE_ARGS, extra_link_args=LINK_ARGS, include_dirs=INCLUDE_DIRS, libraries=LIBRARIES, ) ] ext_modules = cythonize( extensions, include_path=INCLUDE_DIRS, compiler_directives={"binding": True, "language_level": 3}, ) distribution = Distribution({ "name": "package", "ext_modules": ext_modules }) cmd = build_ext(distribution) cmd.ensure_finalized() cmd.run() # Copy built extensions back to the project for output in cmd.get_outputs(): output = Path(output) relative_extension = Path("src") / output.relative_to(cmd.build_lib) shutil.copyfile(output, relative_extension) mode = os.stat(relative_extension).st_mode mode |= (mode & 0o444) >> 2 os.chmod(relative_extension, mode) if __name__ == "__main__": build() ``` {{< /tab >}} {{< tab tabID="cython-src-tree" >}} ``` scripts/ └── build-extension.py src/ └── package ├── example.pyx └── __init__.py ``` {{< /tab >}} {{< /tabs >}} ### Meson {{< tabs tabTotal="2" tabID1="meson-pyproject" tabName1="pyproject.toml" tabID2="meson-build-script" tabName2="build-extension.py">}} {{< tab tabID="meson-pyproject" >}} ```toml [tool.poetry.build] script = "build-extension.py" [build-system] requires = ["poetry-core", "meson"] build-backend = "poetry.core.masonry.api" ``` {{< /tab >}} {{< tab tabID="meson-build-script" >}} ```py from __future__ import annotations import subprocess from pathlib import Path def meson(*args): subprocess.call(["meson", *args]) def build(): build_dir = Path(__file__).parent.joinpath("build") build_dir.mkdir(parents=True, exist_ok=True) meson("setup", build_dir.as_posix()) meson("compile", "-C", build_dir.as_posix()) meson("install", "-C", build_dir.as_posix()) if __name__ == "__main__": build() ``` {{< /tab >}} {{< /tabs >}} ### Maturin {{< tabs tabTotal="2" tabID1="maturin-pyproject" tabName1="pyproject.toml" tabID2="maturin-build-script" tabName2="build-extension.py">}} {{< tab tabID="maturin-pyproject" >}} ```toml [tool.poetry.build] script = "build-extension.py" [build-system] requires = ["poetry-core", "maturin"] build-backend = "poetry.core.masonry.api" ``` {{< /tab >}} {{< tab tabID="maturin-build-script" >}} ```py import os import shlex import shutil import subprocess import zipfile from pathlib import Path def maturin(*args): subprocess.call(["maturin", *list(args)]) def build(): build_dir = Path(__file__).parent.joinpath("build") build_dir.mkdir(parents=True, exist_ok=True) wheels_dir = Path(__file__).parent.joinpath("target/wheels") if wheels_dir.exists(): shutil.rmtree(wheels_dir) cargo_args = [] if os.getenv("MATURIN_BUILD_ARGS"): cargo_args = shlex.split(os.getenv("MATURIN_BUILD_ARGS", "")) maturin("build", "-r", *cargo_args) # We won't use the wheel built by maturin directly since # we want Poetry to build it, but we need to retrieve the # compiled extensions from the maturin wheel. wheel = next(iter(wheels_dir.glob("*.whl"))) with zipfile.ZipFile(wheel.as_posix()) as whl: whl.extractall(wheels_dir.as_posix()) for extension in wheels_dir.rglob("**/*.so"): shutil.copyfile(extension, Path(__file__).parent.joinpath(extension.name)) shutil.rmtree(wheels_dir) if __name__ == "__main__": build() ``` {{< /tab >}} {{< /tabs >}} ## FAQ ### When is my build script executed? If your project uses a build script, it is run implicitly in the following scenarios. 1. When `poetry install` is run, it is executed prior to installing the project's root package. 2. When `poetry build` is run, it is executed prior to building distributions. 3. When a PEP 517 build is triggered from source or sdist by another build frontend. ### How does Poetry ensure my build script's dependencies are met? Prior to executing the build script, Poetry creates a temporary virtual environment with your project's active Python version and then installs all dependencies specified under `build-system.requires` into this environment. It should be noted that no packages will be present in this environment at the time of creation. ### Can I store the build script in a subdirectory? Yes you can. If storing the script in a subdirectory, your `pyproject.toml` might look something like this. ```toml [tool.poetry] ... packages = [ { include = "package", from = "src"} ] include = [ { path = "src/package/**/*.so", format = "wheel" }, ] # exclude any intermediate source files exclude = [ "**/*.c" ] [tool.poetry.build] script = "scripts/build-extension.py" ``` ================================================ FILE: docs/cli.md ================================================ --- title: "Commands" draft: false type: docs layout: single menu: docs: weight: 30 --- # Commands You've already learned how to use the command-line interface to do some things. This chapter documents all the available commands. To get help from the command-line, simply call `poetry` to see the complete list of commands, then `--help` combined with any of those can give you more information. ## Global Options * `--verbose (-v|vv|vvv)`: Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and "-vvv" for debug. {{% note %}} You can also set the verbosity level using the `SHELL_VERBOSITY` environment variable. This is useful in CI/CD pipelines or scripts where you cannot easily modify command-line arguments. | Value | Equivalent | Description | |--------|------------|---------------------| | `-1` | `-q` | Quiet mode | | `0` | (default) | Normal output | | `1` | `-v` | Verbose output | | `2` | `-vv` | More verbose output | | `3` | `-vvv` | Debug output | {{% /note %}} * `--help (-h)` : Display help information. * `--quiet (-q)` : Do not output any message. * `--ansi`: Force ANSI output. * `--no-ansi`: Disable ANSI output. * `--version (-V)`: Display this application version. * `--no-interaction (-n)`: Do not ask any interactive question. * `--no-plugins`: Disables plugins. * `--no-cache`: Disables Poetry source caches. * `--directory=DIRECTORY (-C)`: The working directory for the Poetry command (defaults to the current working directory). All command-line arguments will be resolved relative to the given directory. * `--project=PROJECT (-P)`: Specify another path as the project root. All command-line arguments will be resolved relative to the current working directory or directory specified using `--directory` option if used. ## about The `about` command displays global information about Poetry, including the current version and version of `poetry-core`. ```bash poetry about ``` ## add The `add` command adds required packages to your `pyproject.toml` and installs them. If you do not specify a version constraint, poetry will attempt to use the latest version. ```bash poetry add requests pendulum ``` {{% note %}} A package is looked up, by default, only from [PyPI](https://pypi.org). You can modify the default source (PyPI); or add and use [Supplemental Package Sources]({{< relref "repositories/#supplemental-package-sources" >}}) or [Explicit Package Sources]({{< relref "repositories/#explicit-package-sources" >}}). For more information, refer to the [Package Sources]({{< relref "repositories/#package-sources" >}}) documentation. {{% /note %}} You can also specify a constraint when adding a package: ```bash # Allow >=2.0.5, <3.0.0 versions poetry add pendulum@^2.0.5 # Allow >=2.0.5, <2.1.0 versions poetry add pendulum@~2.0.5 # Allow >=2.0.5 versions, without upper bound poetry add "pendulum>=2.0.5" # Allow only 2.0.5 version poetry add pendulum==2.0.5 ``` {{% note %}} See the [Dependency specification]({{< relref "dependency-specification#using-the--operator" >}}) page for more information about the `@` operator. {{% /note %}} If you try to add a package that is already present, you will get an error. However, if you specify a constraint, like above, the dependency will be updated by using the specified constraint. If you want to get the latest version of an already present dependency, you can use the special `latest` constraint: ```bash poetry add pendulum@latest ``` {{% note %}} See the [Dependency specification]({{< relref "dependency-specification" >}}) for more information on setting the version constraints for a package. {{% /note %}} You can also add `git` dependencies: ```bash poetry add git+https://github.com/sdispater/pendulum.git ``` or use ssh instead of https: ```bash poetry add git+ssh://git@github.com/sdispater/pendulum.git # or alternatively: poetry add git+ssh://git@github.com:sdispater/pendulum.git ``` If you need to checkout a specific branch, tag or revision, you can specify it when using `add`: ```bash poetry add git+https://github.com/sdispater/pendulum.git#develop poetry add git+https://github.com/sdispater/pendulum.git#2.0.5 # or using SSH instead: poetry add git+ssh://git@github.com:sdispater/pendulum.git#develop poetry add git+ssh://git@github.com:sdispater/pendulum.git#2.0.5 ``` or reference a subdirectory: ```bash poetry add git+https://github.com/myorg/mypackage_with_subdirs.git@main#subdirectory=subdir ``` You can also add a local directory or file: ```bash poetry add ./my-package/ poetry add ../my-package/dist/my-package-0.1.0.tar.gz poetry add ../my-package/dist/my_package-0.1.0.whl ``` If you want the dependency to be installed in editable mode you can use the `--editable` option. ```bash poetry add --editable ./my-package/ poetry add --editable git+ssh://github.com/sdispater/pendulum.git#develop ``` Alternatively, you can specify it in the `pyproject.toml` file. It means that changes in the local directory will be reflected directly in environment. ```toml [tool.poetry.dependencies] my-package = {path = "../my/path", develop = true} ``` {{% note %}} The `develop` attribute is a Poetry-specific feature, so it is not included in the package distribution metadata. In other words, it is only considered when using Poetry to install the project. {{% /note %}} If the package(s) you want to install provide extras, you can specify them when adding the package: ```bash poetry add "requests[security,socks]" poetry add "requests[security,socks]~=2.22.0" poetry add "git+https://github.com/pallets/flask.git@1.1.1[dotenv,dev]" ``` {{% warning %}} Some shells may treat square braces (`[` and `]`) as special characters. It is suggested to always quote arguments containing these characters to prevent unexpected shell expansion. {{% /warning %}} If you want to add a package to a specific group of dependencies, you can use the `--group (-G)` option: ```bash poetry add mkdocs --group docs ``` See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for more information about dependency groups. #### Options * `--group (-G)`: The group to add the dependency to. * `--dev (-D)`: Add package as development dependency. (shortcut for `-G dev`) * `--editable (-e)`: Add vcs/path dependencies as editable. * `--extras (-E)`: Extras to activate for the dependency. (multiple values allowed) * `--optional`: Add as an optional dependency to an extra. * `--python`: Python version for which the dependency must be installed. * `--platform`: Platforms for which the dependency must be installed. * `--markers`: Environment markers which describe when the dependency should be installed. * `--source`: Name of the source to use to install the package. * `--allow-prereleases`: Accept prereleases. * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). * `--lock`: Do not perform install (only update the lockfile). ## build The `build` command builds the source and wheels archives. ```bash poetry build ``` The command will trigger the build system defined in the `pyproject.toml` file according to [PEP 517](https://peps.python.org/pep-0517/). If necessary the build process happens in an isolated environment. #### Options * `--format (-f)`: Limit the format to either `wheel` or `sdist`. * `--clean`: Clean output directory before building. * `--local-version (-l)`: Add or replace a local version label to the build (deprecated). * `--output (-o)`: Set output directory for build artifacts. Default is `dist`. * `--config-settings== (-c)`: Config settings to be passed to the build back-end. (multiple allowed) {{% note %}} When using `--local-version`, the identifier must be [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers) compliant. This is useful for adding build numbers, platform specificities, etc. for private packages. `--local-version` is deprecated and will be removed in a future version of Poetry. Use `--config-settings local-version=` instead. {{% /note %}} {{% warning %}} Local version identifiers SHOULD NOT be used when publishing upstream projects to a public index server, but MAY be used to identify private builds created directly from the project source. See [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers) for more information. {{% /warning %}} ## cache The `cache` command groups subcommands to interact with Poetry's cache. ### cache clear The `cache clear` command removes packages from cached repositories. For example, to clear the whole cache of packages from all repositories, run: ```bash poetry cache clear --all ``` To only clear all packages from the `PyPI` repository, run: ```bash poetry cache clear PyPI --all ``` To only remove a specific package from a cache, you have to specify the cache entry in the following form `cache:package:version`: ```bash poetry cache clear PyPI:requests:2.24.0 ``` ### cache list The `cache list` command lists Poetry's available caches. ```bash poetry cache list ``` ## check The `check` command validates the content of the `pyproject.toml` file and its consistency with the `poetry.lock` file. It returns a detailed report if there are any errors. {{% note %}} This command is also available as a pre-commit hook. See [pre-commit hooks]({{< relref "pre-commit-hooks#poetry-check">}}) for more information. {{% /note %}} ```bash poetry check ``` #### Options * `--lock`: Verifies that `poetry.lock` exists for the current `pyproject.toml`. * `--strict`: Fail if check reports warnings. ## config The `config` command allows you to edit poetry config settings and repositories. ```bash poetry config --list ``` ### Usage ````bash poetry config [options] [setting-key] [setting-value1] ... [setting-valueN] ```` `setting-key` is a configuration option name and `setting-value1` is a configuration value. See [Configuration]({{< relref "configuration" >}}) for all available settings. {{% warning %}} Use `--` to terminate option parsing if your values may start with a hyphen (`-`), e.g. ```bash poetry config http-basic.custom-repo gitlab-ci-token -- ${GITLAB_JOB_TOKEN} ``` Without `--` this command will fail if `${GITLAB_JOB_TOKEN}` starts with a hyphen. {{% /warning%}} #### Options * `--unset`: Remove the configuration element named by `setting-key`. * `--list`: Show the list of current config variables. * `--local`: Set/Get settings that are specific to a project (in the local configuration file `poetry.toml`). * `--migrate`: Migrate outdated configuration settings. ## debug The `debug` command groups subcommands that are useful for, as the name suggests, debugging issues you might have when using Poetry with your projects. ### debug info The `debug info` command shows debug information about Poetry and your project's virtual environment. ### debug resolve The `debug resolve` command helps when debugging dependency resolution issues. The command attempts to resolve your dependencies and list the chosen packages and versions. ### debug tags The `debug tags` command is useful when you want to see the supported packaging tags for your project's active virtual environment. This is useful when Poetry cannot install any known binary distributions for a dependency. ## env The `env` command groups subcommands to interact with the virtualenvs associated with a specific project. See [Managing environments]({{< relref "managing-environments" >}}) for more information about these commands. ### env activate The `env activate` command prints the command to activate a virtual environment in your current shell. {{% note %}} This command does not activate the virtual environment, but only displays the activation command, for more information on how to use this command see [here]({{< relref "managing-environments#activating-the-environment" >}}). {{% /note %}} ### env info The `env info` command displays information about the current environment. #### Options * `--path (-p)`: Only display the environment's path. * `--executable (-e)`: Only display the environment's python executable path. ### env list The `env list` command lists all virtualenvs associated with the current project. #### Options * `--full-path`: Output the full paths of the virtualenvs. ### env remove The `env remove` command removes virtual environments associated with the project. You can specify multiple Python executables or virtual environment names to remove all matching ones. Alternatively, you can remove all associated virtual environments using the `--all` option. {{% note %}} If `virtualenvs.in-project` config is set to `true`, no argument or option is required. Your in project virtual environment is removed. {{% /note %}} #### Arguments * `python`: The python executables associated with, or names of the virtual environments which are to be removed. Can be specified multiple times. #### Options * `--all`: Remove all managed virtual environments associated with the project. ### env use The `env use` command activates or creates a new virtualenv for the current project. #### Arguments * `python`: The python executable to use. This can be a version number (if not on Windows) or a path to the python binary. ## export {{% warning %}} This command is provided by the [Export Poetry Plugin](https://github.com/python-poetry/poetry-plugin-export). The plugin is no longer installed by default with Poetry 2.0. See [Using plugins]({{< relref "plugins#using-plugins" >}}) for information on how to install a plugin. As described in [Project plugins]({{< relref "plugins#project-plugins" >}}), you can also define in your `pyproject.toml` that the plugin is required for the development of your project: ```toml [tool.poetry.requires-plugins] poetry-plugin-export = ">=1.8" ``` {{% /warning %}} {{% note %}} The `export` command is also available as a pre-commit hook. See [pre-commit hooks]({{< relref "pre-commit-hooks#poetry-export" >}}) for more information. {{% /note %}} ## help The `help` command displays global help, or help for a specific command. To display global help: ```bash poetry help ``` To display help for a specific command, for instance `show`: ```bash poetry help show ``` {{% note %}} The `--help` option can also be passed to any command to get help for a specific command. For instance: ```bash poetry show --help ``` {{% /note %}} ## init This command will help you create a `pyproject.toml` file interactively by prompting you to provide basic information about your package. It will interactively ask you to fill in the fields, while using some smart defaults. ```bash poetry init ``` #### Options * `--name`: Name of the package. * `--description`: Description of the package. * `--author`: Author of the package. * `--python` Compatible Python versions. * `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`. * `--dev-dependency`: Development requirements, see `--dependency`. ## install The `install` command reads the `pyproject.toml` file from the current project, resolves the dependencies, and installs them. ```bash poetry install ``` If there is a `poetry.lock` file in the current directory, it will use the exact versions from there instead of resolving them. This ensures that everyone using the library will get the same versions of the dependencies. If there is no `poetry.lock` file, Poetry will create one after dependency resolution. {{% note %}} **When to use `install` vs `update`:** - Use `poetry install` to install dependencies as specified in `poetry.lock` (or resolve dependencies and create the lock file if it is missing). This is what you run after cloning a project. For reproducible installs, prefer `poetry sync`, which also removes packages that are not in the lock file. - Use `poetry update` when you want to update dependencies to their latest versions (within the constraints from the `pyproject.toml`) and refresh `poetry.lock`. {{% /note %}} {{% note %}} Normally, you should prefer `poetry sync` to `poetry install` to avoid untracked outdated packages. However, if you have set `virtualenvs.create = false` to install dependencies into your system environment, which is discouraged, or `virtualenvs.options.system-site-packages = true` to make system site-packages available in your virtual environment, you should use `poetry install` because `poetry sync` will normally not work well in these cases. {{% /note %}} If you want to exclude one or more dependency groups for the installation, you can use the `--without` option. ```bash poetry install --without test,docs ``` You can also select optional dependency groups with the `--with` option. ```bash poetry install --with test,docs ``` To install all dependency groups including the optional groups, use the ``--all-groups`` flag. ```bash poetry install --all-groups ``` It's also possible to only install specific dependency groups by using the `only` option. ```bash poetry install --only test,docs ``` To only install the project itself with no dependencies, use the `--only-root` flag. ```bash poetry install --only-root ``` See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for more information about dependency groups. You can also specify the extras you want installed by passing the `-E|--extras` option (See [Extras]({{< relref "pyproject#extras" >}}) for more info). Pass `--all-extras` to install all defined extras for a project. ```bash poetry install --extras "mysql pgsql" poetry install -E mysql -E pgsql poetry install --all-extras ``` Any extras not specified will be kept but not installed: ```bash poetry install --extras "A B" # C is kept if already installed ``` If you want to remove unspecified extras, use the `sync` command. By default `poetry` will install your project's package every time you run `install`: ```bash $ poetry install Installing dependencies from lock file No dependencies to install or update - Installing (x.x.x) ``` If you want to skip this installation, use the `--no-root` option. ```bash poetry install --no-root ``` Similar to `--no-root` you can use `--no-directory` to skip directory path dependencies: ```bash poetry install --no-directory ``` This is mainly useful for caching in CI or when building Docker images. See the [FAQ entry]({{< relref "faq#poetry-busts-my-docker-cache-because-it-requires-me-to-copy-my-source-files-in-before-installing-3rd-party-dependencies" >}}) for more information on this option. By default `poetry` does not compile Python source files to bytecode during installation. This speeds up the installation process, but the first execution may take a little more time because Python then compiles source files to bytecode automatically. If you want to compile source files to bytecode during installation, you can use the `--compile` option: ```bash poetry install --compile ``` #### Options * `--without`: The dependency groups to ignore. * `--with`: The optional dependency groups to include. * `--only`: The only dependency groups to include. * `--only-root`: Install only the root project, exclude all dependencies. * `--sync`: Synchronize the environment with the locked packages and the specified groups. (**Deprecated**, use `poetry sync` instead) * `--no-root`: Do not install the root package (your project). * `--no-directory`: Skip all directory path dependencies (including transitive ones). * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). * `--extras (-E)`: Features to install (multiple values allowed). * `--all-extras`: Install all extra features (conflicts with `--extras`). * `--all-groups`: Install dependencies from all groups (conflicts with `--only`, `--with`, and `--without`). * `--compile`: Compile Python source files to bytecode. {{% note %}} When `--only` is specified, `--with` and `--without` options are ignored. {{% /note %}} ## list The `list` command displays all the available Poetry commands. ```bash poetry list ``` ## lock This command locks (without installing) the dependencies specified in `pyproject.toml`. {{% note %}} By default, packages that have already been added to the lock file before will not be updated. To update all dependencies to the latest available compatible versions, use `poetry update --lock` or `poetry lock --regenerate`, which normally produce the same result. This command is also available as a pre-commit hook. See [pre-commit hooks]({{< relref "pre-commit-hooks#poetry-lock">}}) for more information. {{% /note %}} ```bash poetry lock ``` #### Options * `--regenerate`: Ignore existing lock file and overwrite it with a new lock file created from scratch. ## new This command will help you kickstart your new Python project by creating a new Poetry project. By default, a `src` layout is chosen. ```bash poetry new my-package ``` will create a folder as follows: ```text my-package ├── pyproject.toml ├── README.md ├── src │ └── my_package │ └── __init__.py └── tests └── __init__.py ``` If you want to name your project differently than the folder, you can pass the `--name` option: ```bash poetry new my-folder --name my-package ``` If you want to use a `flat` project layout, you can use the `--flat` option: ```bash poetry new --flat my-package ``` That will create a folder structure as follows: ```text my-package ├── pyproject.toml ├── README.md ├── my_package │ └── __init__.py └── tests └── __init__.py ``` {{% note %}} For an overview of the differences between `flat` and `src` layouts, please see [here](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). {{% /note %}} The `--name` option is smart enough to detect namespace packages and create the required structure for you. ```bash poetry new --name my.package my-package ``` will create the following structure: ```text my-package ├── pyproject.toml ├── README.md ├── src │ └── my │ └── package │ └── __init__.py └── tests └── __init__.py ``` #### Options * `--interactive (-i)`: Allow interactive specification of project configuration. * `--name`: Set the resulting package name. * `--flat`: Use the flat layout for the project. * `--readme`: Specify the readme file extension. Default is `md`. If you intend to publish to PyPI keep the [recommendations for a PyPI-friendly README](https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/) in mind. * `--description`: Description of the package. * `--author`: Author of the package. * `--python` Compatible Python versions. * `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`. * `--dev-dependency`: Development requirements, see `--dependency`. ## publish This command publishes the package, previously built with the [`build`](#build) command, to the remote repository. It will automatically register the package before uploading if this is the first time it is submitted. ```bash poetry publish ``` It can also build the package if you pass it the `--build` option. {{% note %}} See [Publishable Repositories]({{< relref "repositories/#publishable-repositories" >}}) for more information on how to configure and use publishable repositories. {{% /note %}} {{% warning %}} Only artifacts of the latest version of your package in the dist directory will be uploaded. Older versions from previous builds as well as artifacts of other packages are ignored. {{% /warning %}} #### Options * `--repository (-r)`: The repository to register the package to (default: `pypi`). Should match a repository name set by the [`config`](#config) command. * `--username (-u)`: The username to access the repository. * `--password (-p)`: The password to access the repository. * `--cert`: Certificate authority to access the repository. * `--client-cert`: Client certificate to access the repository. * `--dist-dir`: Dist directory where built artifacts are stored. Default is `dist`. * `--build`: Build the package before publishing. * `--dry-run`: Perform all actions except upload the package. * `--skip-existing`: Ignore errors from files already existing in the repository. {{% note %}} See [Configuring Credentials]({{< relref "repositories/#configuring-credentials" >}}) for more information on how to configure credentials. {{% /note %}} ## python The `python` namespace groups subcommands to manage Python versions. {{% warning %}} This is an experimental feature, and can change behaviour in upcoming releases. {{% /warning %}} *Introduced in 2.1.0* ### python install The `python install` command installs the specified Python version from the Python Standalone Builds project. ```bash poetry python install ``` #### Options * `--clean (-c)`: Clean up installation if check fails. * `--free-threaded (-t)`: Use free-threaded version if available. (Same as requesting a version with trailing "t".) * `--implementation (-i)`: Python implementation to use. (cpython, pypy) * `--reinstall (-r)`: Reinstall if installation already exists. ### python list The `python list` command shows Python versions available in the environment. This includes both installed and discovered System managed and Poetry managed installations. ```bash poetry python list ``` #### Options * `--all (-a)`: List all versions, including those available for download. * `--free-threaded (-t)`: List only free-threaded Python versions. * `--implementation (-i)`: Python implementation to search for. * `--managed (-m)`: List only Poetry managed Python versions. ### python remove The `python remove` command removes the specified Python version if managed by Poetry. ```bash poetry python remove ``` #### Options * `--free-threaded (-t)`: Remove free-threaded version (Same as requesting a version with trailing "t".) * `--implementation (-i)`: Python implementation to remove. (cpython, pypy) ## remove The `remove` command removes a package from the current list of installed packages. ```bash poetry remove pendulum ``` If you want to remove a package from a specific group of dependencies, you can use the `--group (-G)` option: ```bash poetry remove mkdocs --group docs ``` See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for more information about dependency groups. #### Options * `--group (-G)`: The group to remove the dependency from. * `--dev (-D)`: Removes a package from the development dependencies. (shortcut for `-G dev`) * `--dry-run` : Outputs the operations but will not execute anything (implicitly enables `--verbose`). * `--lock`: Do not perform operations (only update the lockfile). ## run The `run` command executes the given command inside the project's virtualenv. ```bash poetry run python -V ``` It can also execute one of the scripts defined in `pyproject.toml`. So, if you have a script defined like this: {{< tabs tabTotal="2" tabID1="script-project" tabID2=script-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="script-project" >}} ```toml [project] # ... [project.scripts] my-script = "my_module:main" ``` {{< /tab >}} {{< tab tabID="script-poetry" >}} ```toml [tool.poetry.scripts] my-script = "my_module:main" ``` {{< /tab >}} {{< /tabs >}} You can execute it like so: ```bash poetry run my-script ``` Note that this command has no option. ## search This command searches for packages on a remote index. ```bash poetry search requests pendulum ``` {{% note %}} PyPI no longer allows for the search of packages without a browser. Please use https://pypi.org/search (via a browser) instead. For more information, please see [warehouse documentation](https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods) and this [discussion](https://discuss.python.org/t/fastly-interfering-with-pypi-search/73597/6). {{% /note %}} ## self The `self` namespace groups subcommands to manage the Poetry installation itself. {{% note %}} Use of these commands will create the required `pyproject.toml` and `poetry.lock` files in your [configuration directory]({{< relref "configuration" >}}). {{% /note %}} {{% warning %}} Especially on Windows, `self` commands that update or remove packages may be problematic so that other methods for installing plugins and updating Poetry are recommended. See [Using plugins]({{< relref "plugins#using-plugins" >}}) and [Installing Poetry]({{< relref "docs#installation" >}}) for more information. {{% /warning %}} ### self add The `self add` command installs Poetry plugins and make them available at runtime. Additionally, it can also be used to upgrade Poetry's own dependencies or inject additional packages into the runtime environment {{% note %}} The `self add` command works exactly like the [`add` command](#add). However, is different in that the packages managed are for Poetry's runtime environment. The package specification formats supported by the `self add` command are the same as the ones supported by the [`add` command](#add). {{% /note %}} For example, to install the `poetry-plugin-export` plugin, you can run: ```bash poetry self add poetry-plugin-export ``` To update to the latest `poetry-core` version, you can run: ```bash poetry self add poetry-core@latest ``` To add a keyring provider `artifacts-keyring`, you can run: ```bash poetry self add artifacts-keyring ``` #### Options * `--editable (-e)`: Add vcs/path dependencies as editable. * `--extras (-E)`: Extras to activate for the dependency. (multiple values allowed) * `--allow-prereleases`: Accept prereleases. * `--source`: Name of the source to use to install the package. * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). ### self install The `self install` command ensures all additional packages specified are installed in the current runtime environment. {{% note %}} The `self install` command works similar to the [`install` command](#install). However, it is different in that the packages managed are for Poetry's runtime environment. {{% /note %}} ```bash poetry self install ``` #### Options * `--sync`: Synchronize the environment with the locked packages and the specified groups. (**Deprecated**, use `poetry self sync` instead) * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). ### self lock The `self lock` command reads this Poetry installation's system `pyproject.toml` file. The system dependencies are locked in the corresponding `poetry.lock` file. ```bash poetry self lock ``` #### Options * `--regenerate`: Ignore existing lock file and overwrite it with a new lock file created from scratch. ### self remove The `self remove` command removes an installed addon package. ```bash poetry self remove poetry-plugin-export ``` #### Options * `--dry-run`: Outputs the operations but will not execute anything (implicitly enables `--verbose`). ### self show The `self show` command behaves similar to the show command, but working within Poetry's runtime environment. This lists all packages installed within the Poetry install environment. To show only additional packages that have been added via self add and their dependencies use `self show --addons`. ```bash poetry self show ``` #### Options * `--addons`: List only add-on packages installed. * `--tree`: List the dependencies as a tree. * `--latest (-l)`: Show the latest version. * `--outdated (-o)`: Show the latest version but only for packages that are outdated. * `--format (-f)`: Specify the output format (`json` or `text`). Default is `text`. `json` cannot be combined with the `--tree` option. ### self show plugins The `self show plugins` command lists all the currently installed plugins. ```bash poetry self show plugins ``` ### self sync The `self sync` command ensures all additional (and no other) packages specified are installed in the current runtime environment. {{% note %}} The `self sync` command works similar to the [`sync` command](#sync). However, it is different in that the packages managed are for Poetry's runtime environment. {{% /note %}} ```bash poetry self sync ``` #### Options * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). ### self update The `self update` command updates Poetry version in its current runtime environment. {{% note %}} The `self update` command works exactly like the [`update` command](#update). However, is different in that the packages managed are for Poetry's runtime environment. {{% /note %}} ```bash poetry self update ``` #### Options * `--preview`: Allow the installation of pre-release versions. * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). ## shell The `shell` command was moved to a plugin: [`poetry-plugin-shell`](https://github.com/python-poetry/poetry-plugin-shell) ## show To list all the available packages, you can use the `show` command. ```bash poetry show ``` If you want to see the details of a certain package, you can pass the package name. ```bash poetry show pendulum name : pendulum version : 1.4.2 description : Python datetimes made easy dependencies - python-dateutil >=2.6.1 - tzlocal >=1.4 - pytzdata >=2017.2.2 required by - calendar requires >=1.4.0 ``` #### Options * `--without`: The dependency groups to ignore. * `--why`: When showing the full list, or a `--tree` for a single package, display whether they are a direct dependency or required by other packages. * `--with`: The optional dependency groups to include. * `--only`: The only dependency groups to include. * `--tree`: List the dependencies as a tree. * `--latest (-l)`: Show the latest version. * `--outdated (-o)`: Show the latest version but only for packages that are outdated. * `--all (-a)`: Show all packages (even those not compatible with current system). * `--top-level (-T)`: Only show explicitly defined packages. * `--no-truncate`: Do not truncate the output based on the terminal width. * `--format (-f)`: Specify the output format (`json` or `text`). Default is `text`. `json` cannot be combined with the `--tree` option. {{% note %}} When `--only` is specified, `--with` and `--without` options are ignored. {{% /note %}} ## source The `source` namespace groups subcommands to manage repository sources for a Poetry project. ### source add The `source add` command adds source configuration to the project. For example, to add the `pypi-test` source, you can run: ```bash poetry source add --priority supplemental pypi-test https://test.pypi.org/simple/ ``` You cannot use the name `pypi` for a custom repository as it is reserved for use by the default PyPI source. However, you can set the priority of PyPI: ```bash poetry source add --priority=explicit pypi ``` #### Options * `--priority`: Set the priority of this source. Accepted values are: [`primary`]({{< relref "repositories#primary-package-sources" >}}), [`supplemental`]({{< relref "repositories#supplemental-package-sources" >}}), and [`explicit`]({{< relref "repositories#explicit-package-sources" >}}). Refer to the dedicated sections in [Repositories]({{< relref "repositories" >}}) for more information. ### source show The `source show` command displays information on all configured sources for the project. ```bash poetry source show ``` Optionally, you can show information of one or more sources by specifying their names. ```bash poetry source show pypi-test ``` {{% note %}} This command will only show sources configured via the `pyproject.toml` and does not include the implicit default PyPI. {{% /note %}} ### source remove The `source remove` command removes a configured source from your `pyproject.toml`. ```bash poetry source remove pypi-test ``` ## sync The `sync` command makes sure that the project's environment is in sync with the `poetry.lock` file. It is similar to `poetry install` but it additionally removes packages that are not tracked in the lock file. ```bash poetry sync ``` If there is a `poetry.lock` file in the current directory, it will use the exact versions from there instead of resolving them. This ensures that everyone using the library will get the same versions of the dependencies. If there is no `poetry.lock` file, Poetry will create one after dependency resolution. If you want to exclude one or more dependency groups for the installation, you can use the `--without` option. ```bash poetry sync --without test,docs ``` You can also select optional dependency groups with the `--with` option. ```bash poetry sync --with test,docs ``` To install all dependency groups including the optional groups, use the ``--all-groups`` flag. ```bash poetry sync --all-groups ``` It's also possible to only install specific dependency groups by using the `only` option. ```bash poetry sync --only test,docs ``` To only install the project itself with no dependencies, use the `--only-root` flag. ```bash poetry sync --only-root ``` See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for more information about dependency groups. You can also specify the extras you want installed by passing the `-E|--extras` option (See [Extras]({{< relref "pyproject#extras" >}}) for more info). Pass `--all-extras` to install all defined extras for a project. ```bash poetry sync --extras "mysql pgsql" poetry sync -E mysql -E pgsql poetry sync --all-extras ``` Any extras not specified will always be removed. ```bash poetry sync --extras "A B" # C is removed ``` By default `poetry` will install your project's package every time you run `sync`: ```bash $ poetry sync Installing dependencies from lock file No dependencies to install or update - Installing (x.x.x) ``` If you want to skip this installation, use the `--no-root` option. ```bash poetry sync --no-root ``` Similar to `--no-root` you can use `--no-directory` to skip directory path dependencies: ```bash poetry sync --no-directory ``` This is mainly useful for caching in CI or when building Docker images. See the [FAQ entry]({{< relref "faq#poetry-busts-my-docker-cache-because-it-requires-me-to-copy-my-source-files-in-before-installing-3rd-party-dependencies" >}}) for more information on this option. By default `poetry` does not compile Python source files to bytecode during installation. This speeds up the installation process, but the first execution may take a little more time because Python then compiles source files to bytecode automatically. If you want to compile source files to bytecode during installation, you can use the `--compile` option: ```bash poetry sync --compile ``` #### Options * `--without`: The dependency groups to ignore. * `--with`: The optional dependency groups to include. * `--only`: The only dependency groups to include. * `--only-root`: Install only the root project, exclude all dependencies. * `--no-root`: Do not install the root package (your project). * `--no-directory`: Skip all directory path dependencies (including transitive ones). * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). * `--extras (-E)`: Features to install (multiple values allowed). * `--all-extras`: Install all extra features (conflicts with `--extras`). * `--all-groups`: Install dependencies from all groups (conflicts with `--only`, `--with`, and `--without`). * `--compile`: Compile Python source files to bytecode. {{% note %}} When `--only` is specified, `--with` and `--without` options are ignored. {{% /note %}} ## update In order to get the latest versions of the dependencies and to update the `poetry.lock` file, you should use the `update` command. ```bash poetry update ``` This will resolve all dependencies of the project, write the exact versions into `poetry.lock`, and install them into your environment. {{% note %}} The `update` command **does not** modify your `pyproject.toml` file. It only updates the `poetry.lock` file with the latest compatible versions based on the constraints already defined in `pyproject.toml`. To change version constraints, use the `add` command instead. {{% /note %}} If you just want to update a few packages and not all, you can list them as such: ```bash poetry update requests toml ``` Note that this will not update versions for dependencies outside their [version constraints]({{< relref "dependency-specification#version-constraints" >}}) specified in the `pyproject.toml` file. In other terms, `poetry update foo` will be a no-op if the version constraint specified for `foo` is `~2.3` or `2.3` and `2.4` is available. In order for `foo` to be updated, you must update the constraint, for example `^2.3`. You can do this using the `add` command. #### Options * `--without`: The dependency groups to ignore. * `--with`: The optional dependency groups to include. * `--only`: The only dependency groups to include. * `--dry-run` : Outputs the operations but will not execute anything (implicitly enables `--verbose`). * `--lock` : Do not perform install (only update the lockfile). * `--sync`: Synchronize the environment with the locked packages and the specified groups. {{% note %}} When `--only` is specified, `--with` and `--without` options are ignored. {{% /note %}} ## version This command shows the current version of the project or bumps the version of the project and writes the new version back to `pyproject.toml` if a valid bump rule is provided. The new version should be a valid [PEP 440](https://peps.python.org/pep-0440/) string or a valid bump rule: `patch`, `minor`, `major`, `prepatch`, `preminor`, `premajor`, `prerelease`. {{% note %}} If you would like to use semantic versioning for your project, please see [here]({{< relref "libraries#versioning" >}}). {{% /note %}} The table below illustrates the effect of these rules with concrete examples. | rule | before | after | | ---------- |---------|---------| | major | 1.3.0 | 2.0.0 | | minor | 2.1.4 | 2.2.0 | | patch | 4.1.1 | 4.1.2 | | premajor | 1.0.2 | 2.0.0a0 | | preminor | 1.0.2 | 1.1.0a0 | | prepatch | 1.0.2 | 1.0.3a0 | | prerelease | 1.0.2 | 1.0.3a0 | | prerelease | 1.0.3a0 | 1.0.3a1 | | prerelease | 1.0.3b0 | 1.0.3b1 | The option `--next-phase` allows the increment of prerelease phase versions. | rule | before | after | |-------------------------|----------|----------| | prerelease --next-phase | 1.0.3a0 | 1.0.3b0 | | prerelease --next-phase | 1.0.3b0 | 1.0.3rc0 | | prerelease --next-phase | 1.0.3rc0 | 1.0.3 | #### Options * `--next-phase`: Increment the phase of the current version. * `--short (-s)`: Output the version number only. * `--dry-run`: Do not update pyproject.toml file. ================================================ FILE: docs/community.md ================================================ --- title: "Community" draft: false type: docs layout: single menu: docs: weight: 105 --- # Community ## Badge For any projects using Poetry, you may add its official badge somewhere prominent like the README. [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) **Markdown** ```md [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) ``` **reStructuredText** ```rst .. image:: https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json :alt: Poetry :target: https://python-poetry.org/ ``` ================================================ FILE: docs/configuration.md ================================================ --- title: "Configuration" draft: false type: docs layout: single menu: docs: weight: 40 --- # Configuration Poetry can be configured via the `config` command ([see more about its usage here]({{< relref "cli#config" >}} "config command documentation")) or directly in the `config.toml` file that will be automatically created when you first run that command. This file can typically be found in one of the following directories: - macOS: `~/Library/Application Support/pypoetry` - Windows: `%APPDATA%\pypoetry` For Unix, we follow the XDG spec and support `$XDG_CONFIG_HOME`. That means, by default `~/.config/pypoetry`. ## Local configuration Poetry also provides the ability to have settings that are specific to a project by passing the `--local` option to the `config` command. ```bash poetry config virtualenvs.create false --local ``` {{% note %}} Your local configuration of Poetry application is stored in the `poetry.toml` file, which is separate from `pyproject.toml`. {{% /note %}} {{% note %}} If a setting is defined in both `poetry.toml` (local/project) and `config.toml` (global), the local/project configuration takes precedence over the global configuration. {{% /note %}} {{% warning %}} Be mindful when checking in this file into your repository since it may contain user-specific or sensitive information. {{% /warning %}} ## Listing the current configuration To list the current configuration you can use the `--list` option of the `config` command: ```bash poetry config --list ``` which will give you something similar to this: ```toml cache-dir = "/path/to/cache/directory" virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = true virtualenvs.options.no-pip = false virtualenvs.options.system-site-packages = false virtualenvs.path = "{cache-dir}/virtualenvs" # /path/to/cache/directory/virtualenvs virtualenvs.prompt = "{project_name}-py{python_version}" virtualenvs.use-poetry-python = false ``` ## Displaying a single configuration setting If you want to see the value of a specific setting, you can give its name to the `config` command ```bash poetry config virtualenvs.path ``` For a full list of the supported settings see [Available settings](#available-settings). ## Adding or updating a configuration setting To change or otherwise add a new configuration setting, you can pass a value after the setting's name: ```bash poetry config virtualenvs.path /path/to/cache/directory/virtualenvs ``` For a full list of the supported settings see [Available settings](#available-settings). ## Removing a specific setting If you want to remove a previously set setting, you can use the `--unset` option: ```bash poetry config virtualenvs.path --unset ``` The setting will then retrieve its default value. ## Using environment variables Sometimes, in particular when using Poetry with CI tools, it's easier to use environment variables and not have to execute configuration commands. Poetry supports this and any setting can be set by using environment variables. The environment variables must be prefixed by `POETRY_` and are comprised of the uppercase name of the setting and with dots and dashes replaced by underscore, here is an example: ```bash export POETRY_VIRTUALENVS_PATH=/path/to/virtualenvs/directory ``` This also works for secret settings, like credentials: ```bash export POETRY_HTTP_BASIC_MY_REPOSITORY_PASSWORD=secret ``` ## Migrate outdated configs If Poetry renames or remove config options it might be necessary to migrate explicit set options. This is possible by running: ```bash poetry config --migrate ``` If you need to migrate a local config run: ```bash poetry config --migrate --local ``` ## Default Directories Poetry uses the following default directories: ### Config Directory - Linux: `$XDG_CONFIG_HOME/pypoetry` or `~/.config/pypoetry` - Windows: `%APPDATA%\pypoetry` - macOS: `~/Library/Application Support/pypoetry` You can override the config directory by setting the `POETRY_CONFIG_DIR` environment variable. ### Data Directory - Linux: `$XDG_DATA_HOME/pypoetry` or `~/.local/share/pypoetry` - Windows: `%APPDATA%\pypoetry` - macOS: `~/Library/Application Support/pypoetry` You can override the data directory by setting the `POETRY_DATA_DIR` or `POETRY_HOME` environment variables. If `POETRY_HOME` is set, it will be given higher priority. ### Cache Directory - Linux: `$XDG_CACHE_HOME/pypoetry` or `~/.cache/pypoetry` - Windows: `%LOCALAPPDATA%\pypoetry` - macOS: `~/Library/Caches/pypoetry` You can override the cache directory by setting the `POETRY_CACHE_DIR` environment variable. ## Available settings ### `cache-dir` **Type**: `string` **Environment Variable**: `POETRY_CACHE_DIR` The path to the cache directory used by Poetry. Defaults to one of the following directories: - macOS: `~/Library/Caches/pypoetry` - Windows: `C:\Users\\AppData\Local\pypoetry\Cache` - Unix: `~/.cache/pypoetry` ### `data-dir` **Type**: `string` **Environment Variable**: `POETRY_DATA_DIR` The path to the data directory used by Poetry. - Linux: `$XDG_DATA_HOME/pypoetry` or `~/.local/share/pypoetry` - Windows: `%APPDATA%\pypoetry` - macOS: `~/Library/Application Support/pypoetry` You can override the data directory by setting the `POETRY_DATA_DIR` or `POETRY_HOME` environment variables. If `POETRY_HOME` is set, it will be given higher priority. ### `installer.max-workers` **Type**: `int` **Default**: `number_of_cores + 4` **Environment Variable**: `POETRY_INSTALLER_MAX_WORKERS` *Introduced in 1.2.0* Set the maximum number of workers while using the parallel installer. The `number_of_cores` is determined by `os.cpu_count()`. If this raises a `NotImplementedError` exception, `number_of_cores` is assumed to be 1. If this configuration parameter is set to a value greater than `number_of_cores + 4`, the number of maximum workers is still limited at `number_of_cores + 4`. {{% note %}} This configuration is ignored when `installer.parallel` is set to `false`. {{% /note %}} ### `installer.no-binary` **Type**: `string | boolean` **Default**: `false` **Environment Variable**: `POETRY_INSTALLER_NO_BINARY` *Introduced in 1.2.0* When set, this configuration allows users to disallow the use of binary distribution format for all, none or specific packages. | Configuration | Description | |------------------------|------------------------------------------------------------| | `:all:` or `true` | Disallow binary distributions for all packages. | | `:none:` or `false` | Allow binary distributions for all packages. | | `package[,package,..]` | Disallow binary distributions for specified packages only. | If both `installer.no-binary` and `installer.only-binary` are set, explicit package names will take precedence over `:all:`. {{% note %}} As with all configurations described here, this is a user specific configuration. This means that this is not taken into consideration when a lockfile is generated or dependencies are resolved. This is applied only when selecting which distribution for dependency should be installed into a Poetry managed environment. {{% /note %}} {{% note %}} For project specific usage, it is recommended that this be configured with the `--local`. ```bash poetry config --local installer.no-binary :all: ``` {{% /note %}} {{% note %}} For CI or container environments using [environment variable](#using-environment-variables) to configure this might be useful. ```bash export POETRY_INSTALLER_NO_BINARY=:all: ``` {{% /note %}} {{% warning %}} Unless this is required system-wide, if configured globally, you could encounter slower install times across all your projects if incorrectly set. {{% /warning %}} ### `installer.only-binary` **Type**: `string | boolean` **Default**: `false` **Environment Variable**: `POETRY_INSTALLER_ONLY_BINARY` *Introduced in 2.0.0* When set, this configuration allows users to enforce the use of binary distribution format for all, none or specific packages. {{% note %}} Please refer to [`installer.no-binary`]({{< relref "configuration#installerno-binary" >}}) for information on allowed values, usage instructions and warnings. {{% /note %}} ### `installer.parallel` **Type**: `boolean` **Default**: `true` **Environment Variable**: `POETRY_INSTALLER_PARALLEL` *Introduced in 1.1.4* Use parallel execution when using the new (`>=1.1.0`) installer. ### `installer.build-config-settings.` **Type**: `Serialised JSON with string or list of string properties` **Default**: `None` **Environment Variable**: `POETRY_INSTALLER_BUILD_CONFIG_SETTINGS_` *Introduced in 2.1.0* {{% warning %}} This is an **experimental** configuration and can be subject to changes in upcoming releases until it is considered stable. {{% /warning %}} Configure [PEP 517 config settings](https://peps.python.org/pep-0517/#config-settings) to be passed to a package's build backend if it has to be built from a directory or vcs source; or a source distribution during installation. This is only used when a compatible binary distribution (wheel) is not available for a package. This can be used along with [`installer.no-binary`]({{< relref "configuration#installerno-binary" >}}) option to force a build with these configurations when a dependency of your project with the specified name is being installed. {{% note %}} Poetry does not offer a similar option in the `pyproject.toml` file as these are, in majority of cases, not universal and vary depending on the target installation environment. If you want to use a project specific configuration it is recommended that this configuration be set locally, in your project's `poetry.toml` file. ```bash poetry config --local installer.build-config-settings.grpcio \ '{"CC": "gcc", "--global-option": ["--some-global-option"], "--build-option": ["--build-option1", "--build-option2"]}' ``` If you want to modify a single key, you can do, by setting the same key again. ```bash poetry config --local installer.build-config-settings.grpcio \ '{"CC": "g++"}' ``` {{% /note %}} ### `requests.max-retries` **Type**: `int` **Default**: `0` **Environment Variable**: `POETRY_REQUESTS_MAX_RETRIES` *Introduced in 2.0.0* Set the maximum number of retries in an unstable network. This setting has no effect if the server does not support HTTP range requests. ### `installer.re-resolve` **Type**: `boolean` **Default**: `false` **Environment Variable**: `POETRY_INSTALLER_RE_RESOLVE` *Changed default from `true` to `false` in 2.3.0* *Introduced in 2.0.0* If the config option is _not_ set and the lock file is at least version 2.1 (created by Poetry 2.0 or above), the installer will not re-resolve dependencies but evaluate the locked markers to decide which of the locked dependencies have to be installed into the target environment. ### `python.installation-dir` **Type**: `string` **Default**: `{data-dir}/python` **Environment Variable**: `POETRY_PYTHON_INSTALLATION_DIR` *Introduced in 2.1.0* The directory in which Poetry managed Python versions are installed to. ### `solver.lazy-wheel` **Type**: `boolean` **Default**: `true` **Environment Variable**: `POETRY_SOLVER_LAZY_WHEEL` *Introduced in 1.8.0* Do not download entire wheels to extract metadata but use [HTTP range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests) to only download the METADATA files of wheels. Especially with slow network connections, this setting can speed up dependency resolution significantly. If the cache has already been filled or the server does not support HTTP range requests, this setting makes no difference. ### `system-git-client` **Type**: `boolean` **Default**: `false` **Environment Variable**: `POETRY_SYSTEM_GIT_CLIENT` *Renamed to `system-git-client` in 2.0.0* *Introduced in 1.2.0 as `experimental.system-git-client`* Use system git client backend for git related tasks. Poetry uses `dulwich` by default for git related tasks to not rely on the availability of a git client. If you encounter any problems with it, set to `true` to use the system git backend. ### `virtualenvs.create` **Type**: `boolean` **Default**: `true` **Environment Variable**: `POETRY_VIRTUALENVS_CREATE` Create a new virtual environment if one doesn't already exist. If set to `false`, Poetry will not create a new virtual environment. If it detects an already enabled virtual environment or an existing one in `{cache-dir}/virtualenvs` or `{project-dir}/.venv` it will install dependencies into them, otherwise it will install dependencies into the systems python environment. {{% note %}} If Poetry detects it's running within an activated virtual environment, it will never create a new virtual environment, regardless of the value set for `virtualenvs.create`. {{% /note %}} {{% note %}} Be aware that installing dependencies into the system environment likely upgrade or uninstall existing packages and thus break other applications. Installing additional Python packages after installing the project might break the Poetry project in return. This is why it is recommended to always create a virtual environment. This is also true in Docker containers, as they might contain additional Python packages as well. {{% /note %}} ### `virtualenvs.in-project` **Type**: `boolean` **Default**: `None` **Environment Variable**: `POETRY_VIRTUALENVS_IN_PROJECT` Create the virtualenv inside the project's root directory. If not set explicitly, `poetry` by default will create a virtual environment under `{cache-dir}/virtualenvs` or use the `{project-dir}/.venv` directory if one already exists. If set to `true`, the virtualenv will be created and expected in a folder named `.venv` within the root directory of the project. {{% note %}} If a virtual environment has already been created for the project under `{cache-dir}/virtualenvs`, setting this variable to `true` will not cause `poetry` to create or use a local virtual environment. In order for this setting to take effect for a project already in that state, you must delete the virtual environment folder located in `{cache-dir}/virtualenvs`. You can find out where the current project's virtual environment (if there is one) is stored with the command `poetry env info --path`. {{% /note %}} If set to `false`, `poetry` will ignore any existing `.venv` directory. ### `virtualenvs.options.always-copy` **Type**: `boolean` **Default**: `false` **Environment Variable**: `POETRY_VIRTUALENVS_OPTIONS_ALWAYS_COPY` *Introduced in 1.2.0* If set to `true` the `--always-copy` parameter is passed to `virtualenv` on creation of the virtual environment, so that all needed files are copied into it instead of symlinked. ### `virtualenvs.options.no-pip` **Type**: `boolean` **Default**: `false` **Environment Variable**: `POETRY_VIRTUALENVS_OPTIONS_NO_PIP` *Introduced in 1.2.0* If set to `true` the `--no-pip` parameter is passed to `virtualenv` on creation of the virtual environment. This means when a new virtual environment is created, `pip` will not be installed in the environment. {{% note %}} Poetry, for its internal operations, uses the `pip` wheel embedded in the `virtualenv` package installed as a dependency in Poetry's runtime environment. If a user runs `poetry run pip` when this option is set to `true`, the `pip` the embedded instance of `pip` is used. You can safely set this to `true`, if you desire a virtual environment with no additional packages. This is desirable for production environments. {{% /note %}} ### `virtualenvs.options.system-site-packages` **Type**: `boolean` **Default**: `false` **Environment Variable**: `POETRY_VIRTUALENVS_OPTIONS_SYSTEM_SITE_PACKAGES` Give the virtual environment access to the system site-packages directory. Applies on virtualenv creation. ### `virtualenvs.path` **Type**: `string` **Default**: `{cache-dir}/virtualenvs` **Environment Variable**: `POETRY_VIRTUALENVS_PATH` Directory where virtual environments will be created. {{% note %}} This setting controls the global virtual environment storage path. It most likely will not be useful at the local level. To store virtual environments in the project root, see `virtualenvs.in-project`. {{% /note %}} ### `virtualenvs.prompt` **Type**: `string` **Default**: `{project_name}-py{python_version}` **Environment Variable**: `POETRY_VIRTUALENVS_PROMPT` *Introduced in 1.2.0* Format string defining the prompt to be displayed when the virtual environment is activated. The variables `project_name` and `python_version` are available for formatting. ### `virtualenvs.use-poetry-python` **Type**: `boolean` **Default**: `false` **Environment Variable**: `POETRY_VIRTUALENVS_USE_POETRY_PYTHON` *Introduced in 2.0.0* By default, Poetry will use the activated Python version to create a new virtual environment. If set to `true`, the Python version used during Poetry installation is used. ### `repositories..url` **Type**: `string` **Environment Variable**: `POETRY_REPOSITORIES__URL` Set the repository URL for ``. See [Publishable Repositories]({{< relref "repositories#publishable-repositories" >}}) for more information. ### `http-basic..[username|password]` **Type**: `string` **Environment Variables**: `POETRY_HTTP_BASIC__USERNAME`, `POETRY_HTTP_BASIC__PASSWORD` Set repository credentials (`username` and `password`) for ``. See [Repositories - Configuring credentials]({{< relref "repositories#configuring-credentials" >}}) for more information. ### `pypi-token.` **Type**: `string` **Environment Variable**: `POETRY_PYPI_TOKEN_` Set repository credentials (using an API token) for ``. See [Repositories - Configuring credentials]({{< relref "repositories#configuring-credentials" >}}) for more information. ### `certificates..cert` **Type**: `string | boolean` **Environment Variable**: `POETRY_CERTIFICATES__CERT` Set custom certificate authority for repository ``. See [Repositories - Configuring credentials - Custom certificate authority]({{< relref "repositories#custom-certificate-authority-and-mutual-tls-authentication" >}}) for more information. This configuration can be set to `false`, if TLS certificate verification should be skipped for this repository. ### `certificates..client-cert` **Type**: `string` **Environment Variable**: `POETRY_CERTIFICATES__CLIENT_CERT` Set client certificate for repository ``. See [Repositories - Configuring credentials - Custom certificate authority]({{< relref "repositories#custom-certificate-authority-and-mutual-tls-authentication" >}}) for more information. ### `keyring.enabled` **Type**: `boolean` **Default**: `true` **Environment Variable**: `POETRY_KEYRING_ENABLED` Enable the system keyring for storing credentials. See [Repositories - Configuring credentials]({{< relref "repositories#configuring-credentials" >}}) for more information. ================================================ FILE: docs/contributing.md ================================================ --- title: "Contributing to Poetry" draft: false type: docs layout: single menu: docs: weight: 100 note: "Are you viewing this document on GitHub? For the best experience, view it on the website https://python-poetry.org/docs/contributing." --- # Contributing to Poetry First off, thanks for taking the time to contribute! The following is a set of guidelines for contributing to Poetry on GitHub. These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request. ## How to contribute ### Reporting bugs This section guides you through submitting a bug report for Poetry. Following these guidelines helps maintainers and the community understands your report, reproduces the behavior, and finds related reports. #### Before submitting a bug report * **Check the [FAQ]** for a list of common questions and problems. * **Check the [blog]** for release notes from recent releases, including steps for upgrading and known issues. * **Check that your issue does not already exist** in the [issue tracker]. * **Make sure your issue is really a bug, and is not a support request or question** better suited for [Discussions] or [Discord]. * **Try running your commands with the** `--no-cache` **flag**. * **Try clearing your cache with** `poetry cache clear --all PyPI` **and rerunning your commands**. {{% note %}} If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. {{% /note %}} #### How do I submit a bug report? Bugs concerning Poetry and poetry-core should be submitted to the main [issue tracker], using the correct [issue template]. Explain the problem and make it easy for others to search for and understand: * **Use a clear and descriptive title** for the issue to identify the problem. * **Describe the exact steps which reproduce the problem** in as many details as possible. * **Describe the behavior you observed after following the steps** and point out how this is a bug. * **Explain which behavior you expected to see instead and why.** * **If the problem involves an unexpected error being raised**, execute the problematic command in **debug** mode (with `-vvv` flag). Provide detailed steps for reproduction of your issue: * **Provide your pyproject.toml file** in a [Gist](https://gist.github.com), pastebin, or example repository after removing potential private information like private package repositories or names. * **Provide specific examples to demonstrate the steps to reproduce the issue**. This could be an example repository, a sequence of steps run in a container, or just a pyproject.toml for very simple cases. * **Are you unable to reliably reproduce the issue?** If so, provide details about how often the problem happens and under which conditions it normally happens. Provide more context by answering these questions: * **Did the problem start happening recently** (e.g., after updating to a new version of Poetry) or was this always a problem? * If the problem started happening recently, **can you reproduce the problem in an older version of Poetry?** What's the most recent version in which the problem does not happen? * **Is there anything exotic or unusual about your environment?** This could include use of special container images, newer CPU architectures like Apple Silicon, or corporate proxies that intercept or modify your network traffic. Include details about your configuration and environment: * **Which version of Poetry are you using?** You can get the exact version by running `poetry --version`. * **What version of Python is being used to run Poetry?** Execute the `poetry debug info` to get this information. * **What's the name and version of the OS you're using?** Examples include Ubuntu 22.04 or macOS 12.6. To give others the best chance to understand and reproduce your issue, please be sure to put extra effort into your reproduction steps. You can both rule out local configuration issues on your end, and ensure others can cleanly reproduce your issue if attempt all reproductions in a pristine container (or VM), and provide the steps you performed inside that container/VM in your issue report. ### Suggesting enhancements This section guides you through submitting an enhancement suggestion for Poetry, including completely new features as well as improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions. #### Before submitting a suggested enhancement * **Check the [FAQ]** for a list of common questions and problems. * **Check that your issue does not already exist** in the [issue tracker]. #### How do I submit a suggested enhancement? Suggested enhancements concerning Poetry and poetry-core should be submitted to the main [issue tracker], using the correct [issue template]. * **Use a clear and descriptive title** for the issue to identify the suggestion. * **Provide a detailed description of the proposed enhancement**, with specific steps or examples when possible. * **Describe the current behavior** and **explain which behavior you would like to see instead**, and why. ### Documentation contributions One of the simplest ways to get started contributing to a project is through improving documentation. Poetry is constantly evolving, and this means that sometimes our documentation has gaps. You can help by adding missing sections, editing the existing content to be more accessible, or creating new content such as tutorials, FAQs, etc. {{% note %}} GitHub [Discussions](https://github.com/python-poetry/poetry/discussions) and the [kind/question label](https://github.com/python-poetry/poetry/labels/kind/question) are excellent sources for FAQ candidates. {{% /note %}} Issues pertaining to the documentation are usually marked with the [area/docs label], which will also trigger a preview of the changes as rendered by this website. ### Code contributions #### Picking an issue {{% note %}} If you are a first time contributor, and are looking for an issue to take on, you might want to look for at the [contributing page](https://github.com/python-poetry/poetry/contribute) for candidates. We do our best to curate good issues for first-time contributors there, but do fall behind -- so if you don't see anything good, feel free to ask. {{% /note %}} If you would like to take on an issue, feel free to comment on the issue tagging `@python-poetry/triage`. We are more than happy to discuss solutions on the issue. If you would like help with navigating the code base, are looking for something to work on, or want feedback on a design or change, join us on our [Discord server][Discord] or start a [Discussion][Discussions]. #### Local development Poetry is developed using Poetry. Refer to the [documentation] to install Poetry in your local environment. {{% note %}} Poetry's development toolchain requires Python 3.9 or newer. {{% /note %}} You should first fork the Poetry repository and then clone it locally, so that you can make pull requests against the project. If you are new to Git and pull request-based development, GitHub provides a [guide](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) you will find helpful. Next, you should install Poetry's dependencies, and run the test suite to make sure everything is working as expected: ```bash poetry install poetry run pytest ``` {{% note %}} If you want to see the coverage stats after the tests are complete, use: ```bash poetry run pytest --cov=src/poetry --cov-report term ``` {{% /note %}} When you contribute to Poetry, automated tools will be run to make sure your code is suitable to be merged. Besides pytest, you will need to make sure your code typechecks properly using [mypy](http://mypy-lang.org/): ```bash poetry run mypy ``` Finally, a great deal of linting tools are run on your code, to try and ensure consistent code style and root out common mistakes. The [pre-commit](https://pre-commit.com/) tool is used to install and run these tools, and requires one-time setup: ```bash poetry run pre-commit install ``` pre-commit will now run and check your code every time you make a commit. By default, it will only run on changed files, but you can run it on all files manually (this may be useful if you altered the pre-commit config): ```bash poetry run pre-commit run --all-files ``` #### Pull requests * Fill out the pull request body completely and describe your changes as accurately as possible. The pull request body should be kept up to date as it will usually form the base for the final merge commit and the changelog entry. * Be sure that your pull request contains tests that cover the changed or added code. Tests are generally required for code be to be considered mergeable, and code without passing tests will not be merged. * Ensure your pull request passes the mypy and pre-commit checks. Remember that you can run these tools locally instead of relying on remote CI. * If your changes warrant a documentation change, the pull request must also update the documentation. Make sure to review the documentation preview generated by CI for any rendering issues. {{% note %}} Make sure your branch is [rebased](https://docs.github.com/en/get-started/using-git/about-git-rebase) against the latest base branch. A maintainer might ask you to ensure the branch is up-to-date prior to merging your pull request (especially if there have been CI changes on the base branch), and will also ask you to fix any conflicts. {{% /note %}} All pull requests, unless otherwise instructed, need to be first accepted into the `main` branch. Maintainers will generally decide if any backports to other branches are required, and carry them out as needed. ### Issue triage {{% note %}} If you have an issue that hasn't had any attention, you can ping us `@python-poetry/triage` on the issue. Please give us reasonable time to get to your issue first, and avoid pinging any individuals directly, especially if they are not part of the Poetry team. {{% /note %}} If you are helping with the triage of reported issues, this section provides some useful information to assist you in your contribution. #### Triage steps 1. Determine what area and versions of Poetry the issue is related to, and set the appropriate labels (e.g. `version/x.x.x`, `area/docs`, `area/venv`), and remove the `status/triage` label. 2. If requested information (such as debug logs, pyproject.toml, etc.) is not provided and is relevant, request it from the author. 1. Set the `status/waiting-on-response` label while waiting to hear back from the author. 3. Attempt to reproduce the issue with the reported Poetry version or request further clarification from the author. 4. Ensure the issue is not already resolved. Try reproducing it on the latest stable release, the latest prerelease (if present), and the development branch. 5. If the issue cannot be reproduced, 1. request more reproduction steps and clarification from the issue's author, 2. set the `status/needs-reproduction` label, 3. close the issue if there is no reproduction forthcoming. 6. If the issue can be reproduced, 1. comment on the issue confirming so, 2. set the `status/confirmed` label, 3. if possible, identify the root cause of the issue, 4. if interested, attempt to fix it via a pull request. #### Multiple versions When trying to reproduce issues, you often want to use multiple versions of Poetry at the same time. [pipx](https://pypa.github.io/pipx/) makes this easy to do: ```sh pipx install --suffix @1.2.1 'poetry==1.2.1' pipx install --suffix @1.3.0rc1 'poetry==1.3.0rc1' pipx install --suffix @main 'poetry @ git+https://github.com/python-poetry/poetry' pipx install --suffix @local '/path/to/local/clone/of/poetry' # now you can use any of the chosen versions of Poetry with their configured suffix, e.g. poetry@main --version ``` {{% note %}} Do not forget to `pipx upgrade poetry@main` before using it, to make sure you have the latest changes. {{% /note %}} {{% note %}} This mechanism can also be used to test pull requests by using GitHub's pull request remote refs: ```sh pipx install --suffix @pr1234 git+https://github.com/python-poetry/poetry.git@refs/pull/1234/head ``` {{% /note %}} [Blog]: {{< ref "/blog" >}} [Documentation]: {{< ref "/docs" >}} [FAQ]: {{< relref "faq" >}} [Issue Tracker]: https://github.com/python-poetry/poetry/issues [area/docs label]: https://github.com/python-poetry/poetry/labels/area/docs [kind/question label]: https://github.com/python-poetry/poetry/labels/kind/question [Issue Template]: https://github.com/python-poetry/poetry/issues/new/choose [Discussions]: https://github.com/python-poetry/poetry/discussions [Discord]: https://discord.com/invite/awxPgve ================================================ FILE: docs/dependency-specification.md ================================================ --- title: "Dependency specification" draft: false type: docs layout: single menu: docs: weight: 70 --- # Dependency specification Dependencies for a project can be specified in various forms, which depend on the type of the dependency and on the optional constraints that might be needed for it to be installed. ## `project.dependencies` and `tool.poetry.dependencies` Prior Poetry 2.0, dependencies had to be declared in the `tool.poetry.dependencies` section of the `pyproject.toml` file. ```toml [tool.poetry.dependencies] requests = "^2.13.0" ``` With Poetry 2.0, you should consider using the `project.dependencies` section instead. ```toml [project] # ... dependencies = [ "requests (>=2.23.0,<3.0.0)" ] ``` While dependencies in `tool.poetry.dependencies` are specified using toml tables, dependencies in `project.dependencies` are specified as strings according to [PEP 508](https://peps.python.org/pep-0508/). In many cases, `tool.poetry.dependencies` can be replaced with `project.dependencies`. However, there are some cases where you might still need to use `tool.poetry.dependencies`. For example, if you want to define additional information that is not required for building but only for locking (for example, an explicit source), you can enrich dependency information in the `tool.poetry` section. ```toml [project] # ... dependencies = [ "requests>=2.13.0", ] [tool.poetry.dependencies] requests = { source = "private-source" } ``` When both are specified, `project.dependencies` are used for metadata when building the project, `tool.poetry.dependencies` is only used to enrich `project.dependencies` for locking. Alternatively, you can add `dependencies` to `dynamic` and define your dependencies completely in the `tool.poetry` section. Using only the `tool.poetry` section might make sense in non-package mode when you will not build an sdist or a wheel. ```toml [project] # ... dynamic = [ "dependencies" ] [tool.poetry.dependencies] requests = { version = ">=2.13.0", source = "private-source" } ``` {{% note %}} Another use case for `tool.poetry.dependencies` are relative path dependencies since `project.dependencies` only support absolute paths. {{% /note %}} {{% note %}} Only main dependencies can be specified in the `project` section. Other [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) must still be specified in the `tool.poetry` section. {{% /note %}} ## Version constraints ### Compatible release requirements **Compatible release requirements** specify a minimal version with the ability to update to later versions of the same level. For example, if you specify a major, minor, and patch version, only patch-level changes are allowed. If you only specify a major, and minor version, then minor- and patch-level changes are allowed. `~=1.2.3` is an example of a compatible release requirement. | Requirement | Versions allowed | | ----------- | ---------------- | | ~=1.2.3 | >=1.2.3 <1.3.0 | | ~=1.2 | >=1.2.0 <2.0.0 | ### Wildcard requirements **Wildcard requirements** allow for the latest (dependency-dependent) version where the wildcard is positioned. `*`, `1.*` and `1.2.*` are examples of wildcard requirements. | Requirement | Versions allowed | | ----------- | ---------------- | | * | >=0.0.0 | | 1.* | >=1.0.0 <2.0.0 | | 1.2.* | >=1.2.0 <1.3.0 | ### Inequality requirements **Inequality requirements** allow manually specifying a version range or an exact version to depend on. Here are some examples of inequality requirements: ```text >= 1.2.0 > 1 < 2 != 1.2.3 ``` #### Multiple requirements Multiple version requirements can also be separated with a comma, e.g. `>= 1.2, < 1.5`. ### Exact requirements You can specify the exact version of a package. `1.2.3` is an example of an exact version specification. This will tell Poetry to install this version and this version only. If other dependencies require a different version, the solver will ultimately fail and abort any installation or update procedures. Exact versions can also be specified with `==` according to [PEP 440](https://peps.python.org/pep-0440/). `==1.2.3` is an example of this. ### Caret requirements {{% warning %}} Not supported in `project.dependencies`. When using `poetry add` such constraints are automatically converted into an equivalent constraint. {{% /warning %}} **Caret requirements** allow [SemVer](https://semver.org/) compatible updates to a specified version. An update is allowed if the new version number does not modify the left-most non-zero digit in the major, minor, patch grouping. For instance, if we previously ran `poetry add requests@^2.13.0` and wanted to update the library and ran `poetry update requests`, poetry would update us to version `2.14.0` if it was available, but would not update us to `3.0.0`. If instead, we had specified the version string as `^0.1.13`, poetry would update to `0.1.14` but not `0.2.0`. `0.0.x` is not considered compatible with any other version. Here are some more examples of caret requirements and the versions that would be allowed with them: | Requirement | Versions allowed | | ----------- | ---------------- | | ^1.2.3 | >=1.2.3 <2.0.0 | | ^1.2 | >=1.2.0 <2.0.0 | | ^1 | >=1.0.0 <2.0.0 | | ^0.2.3 | >=0.2.3 <0.3.0 | | ^0.0.3 | >=0.0.3 <0.0.4 | | ^0.0 | >=0.0.0 <0.1.0 | | ^0 | >=0.0.0 <1.0.0 | ### Tilde requirements {{% warning %}} Not supported in `project.dependencies`. When using `poetry add` such constraints are automatically converted into an equivalent constraint. {{% /warning %}} **Tilde requirements** specify a minimal version with some ability to update. If you specify a major, minor, and patch version or only a major and minor version, only patch-level changes are allowed. If you only specify a major version, then minor- and patch-level changes are allowed. `~1.2.3` is an example of a tilde requirement. | Requirement | Versions allowed | | ----------- | ---------------- | | ~1.2.3 | >=1.2.3 <1.3.0 | | ~1.2 | >=1.2.0 <1.3.0 | | ~1 | >=1.0.0 <2.0.0 | ### Using the `@` operator When adding dependencies via `poetry add`, you can use the `@` operator. This is understood similarly to the `==` syntax, but also allows prefixing any specifiers that are valid in `pyproject.toml`. For example: ```shell poetry add "django@^4.0.0" ``` The above would translate to the following entry in `pyproject.toml`: {{< tabs tabTotal="2" tabID1="at-project" tabID2="at-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="at-project" >}} ```toml [project] # ... dependencies = [ "django (>=4.0.0,<5.0.0)", ] ``` {{< /tab >}} {{< tab tabID="at-poetry" >}} ```toml [tool.poetry.dependencies] django = "^4.0.0" ``` {{< /tab >}} {{< /tabs >}} The special keyword `latest` is also understood by the `@` operator: ```shell poetry add django@latest ``` The above would translate to the following entry in `pyproject.toml`, assuming the latest release of `django` is `5.1.3`: {{< tabs tabTotal="2" tabID1="at-latest-project" tabID2="at-latest-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="at-latest-project" >}} ```toml [project] # ... dependencies = [ "django (>=5.1.3,<6.0.0)", ] ``` {{< /tab >}} {{< tab tabID="at-latest-poetry" >}} ```toml [tool.poetry.dependencies] django = "^5.1.3" ``` {{< /tab >}} {{< /tabs >}} #### Extras Extras and `@` can be combined as one might expect (`package[extra]@version`): ```shell poetry add django[bcrypt]@^4.0.0 ``` ## `git` dependencies To depend on a library located in a `git` repository, the minimum information you need to specify is the location of the repository: {{< tabs tabTotal="2" tabID1="git-project" tabID2="git-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="git-project" >}} ```toml [project] # ... dependencies = [ "requests @ git+https://github.com/requests/requests.git", ] ``` {{< /tab >}} {{< tab tabID="git-poetry" >}} In the `tool.poetry` section you use the `git` key: ```toml [tool.poetry.dependencies] requests = { git = "https://github.com/requests/requests.git" } ``` {{< /tab >}} {{< /tabs >}} Since we haven’t specified any other information, Poetry assumes that we intend to use the latest commit on the `main` branch to build our project. You can explicitly specify which branch, commit hash or tagged ref should be used: {{< tabs tabTotal="2" tabID1="git-rev-project" tabID2="git-rev-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="git-rev-project" >}} Append the information to the git url. ```toml [project] # ... dependencies = [ "requests @ git+https://github.com/requests/requests.git@next", "flask @ git+https://github.com/pallets/flask.git@38eb5d3b", "numpy @ git+https://github.com/numpy/numpy.git@v0.13.2", ] ``` {{< /tab >}} {{< tab tabID="git-rev-poetry" >}} Combine the `git` key with the `branch`, `rev` or `tag` key respectively. ```toml [tool.poetry.dependencies] # Get the latest revision on the branch named "next" requests = { git = "https://github.com/kennethreitz/requests.git", branch = "next" } # Get a revision by its commit hash flask = { git = "https://github.com/pallets/flask.git", rev = "38eb5d3b" } # Get a revision by its tag numpy = { git = "https://github.com/numpy/numpy.git", tag = "v0.13.2" } ``` {{< /tab >}} {{< /tabs >}} It's possible to add a package that is located in a subdirectory of the VCS repository. {{< tabs tabTotal="2" tabID1="git-subdir-project" tabID2="git-subdir-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="git-subdir-project" >}} Provide the subdirectory as a URL fragment similarly to what [pip](https://pip.pypa.io/en/stable/topics/vcs-support/#url-fragments) provides. ```toml [project] # ... dependencies = [ "subdir_package @ git+https://github.com/myorg/mypackage_with_subdirs.git#subdirectory=subdir" ] ``` {{< /tab >}} {{< tab tabID="git-subdir-poetry" >}} Use the `subdirectory` key in the `tool.poetry` section: ```toml [tool.poetry.dependencies] # Install a package named `subdir_package` from a folder called `subdir` within the repository subdir_package = { git = "https://github.com/myorg/mypackage_with_subdirs.git", subdirectory = "subdir" } ``` {{< /tab >}} {{< /tabs >}} The corresponding `add` call looks like this: ```bash poetry add "git+https://github.com/myorg/mypackage_with_subdirs.git#subdirectory=subdir" ``` To use an SSH connection, for example, in the case of private repositories, use the following example syntax: {{< tabs tabTotal="2" tabID1="git-ssh-project" tabID2="git-ssh-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="git-ssh-project" >}} ```toml [project] # ... dependencies = [ "pendulum @ git+ssh://git@github.com/sdispater/pendulum.git" ] ``` {{< /tab >}} {{< tab tabID="git-ssh-poetry" >}} ```toml [tool.poetry.dependencies] pendulum = { git = "git@github.com/sdispater/pendulum.git" } ``` {{< /tab >}} {{< /tabs >}} ### Credentials for git dependencies To use HTTP basic authentication with your git repositories, you can configure credentials similar to how [repository credentials]({{< relref "repositories#configuring-credentials" >}}) are configured. ```bash poetry config repositories.git-org-project https://github.com/org/project.git poetry config http-basic.git-org-project username token poetry add git+https://github.com/org/project.git ``` {{% note %}} The default git client used is [Dulwich](https://www.dulwich.io/). We fall back to legacy system git client implementation in cases where [gitcredentials](https://git-scm.com/docs/gitcredentials) is used. This fallback will be removed in a future release where `gitcredentials` helpers can be better supported natively. In cases where you encounter issues with the default implementation, you may wish to explicitly configure the use of the system git client via a shell subprocess call. ```bash poetry config system-git-client true ``` {{% /note %}} ## `path` dependencies {{< tabs tabTotal="2" tabID1="path-project" tabID2="path-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="path-project" >}} In the `project` section, you can only use absolute paths: ```toml [project] # directory dependencies = [ "my-package @ file:///absolute/path/to/my-package" ] # file dependencies = [ "my-package @ file:///absolute/path/to/my-package/dist/my-package-0.1.0.tar.gz" ] ``` {{< /tab >}} {{< tab tabID="path-poetry" >}} To depend on a library located in a local directory or file, you can use the `path` property: ```toml [tool.poetry.dependencies] # directory my-package = { path = "../my-package/", develop = true } # file my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" } ``` To install directory path dependencies in editable mode, use the `develop` keyword and set it to `true`. {{< /tab >}} {{< /tabs >}} ## `url` dependencies `url` dependencies are libraries located on a remote archive. {{< tabs tabTotal="2" tabID1="url-project" tabID2="url-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="url-project" >}} ```toml [project] # ... dependencies = [ "my-package @ https://example.com/my-package-0.1.0.tar.gz" ] ``` {{< /tab >}} {{< tab tabID="url-poetry" >}} Use the `url` property. ```toml [tool.poetry.dependencies] # directory my-package = { url = "https://example.com/my-package-0.1.0.tar.gz" } ``` {{< /tab >}} {{< /tabs >}} The corresponding `add` call is: ```bash poetry add https://example.com/my-package-0.1.0.tar.gz ``` ## Dependency `extras` You can specify [PEP-508 Extras](https://www.python.org/dev/peps/pep-0508/#extras) for a dependency as shown here. {{< tabs tabTotal="2" tabID1="extras-project" tabID2="extras-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="extras-project" >}} ```toml [project] # ... dependencies = [ "gunicorn[gevent] (>=20.1,<21.0)" ] ``` {{< /tab >}} {{< tab tabID="extras-poetry" >}} ```toml [tool.poetry.dependencies] gunicorn = { version = "^20.1", extras = ["gevent"] } ``` {{< /tab >}} {{< /tabs >}} {{% note %}} These activate extra defined for the dependency, to configure an optional dependency for extras in your project refer to [`extras`]({{< relref "pyproject#extras" >}}). {{% /note %}} ## `source` dependencies {{% note %}} It is not possible to define source dependencies in the `project` section. {{% /note %}} To depend on a package from an [alternate repository]({{< relref "repositories#installing-from-private-package-sources" >}}), you can use the `source` property: ```toml [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" priority = "supplemental" [tool.poetry.dependencies] my-cool-package = { version = "*", source = "foo" } ``` with the corresponding `add` call: ```sh poetry add my-cool-package --source foo ``` {{% note %}} In this example, we expect `foo` to be configured correctly. See [using a private repository]({{< relref "repositories#installing-from-private-package-sources" >}}) for further information. {{% /note %}} ## Python restricted dependencies You can also specify that a dependency should be installed only for specific Python versions: {{< tabs tabTotal="2" tabID1="python-restriction-project" tabID2="python-restriction-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="python-restriction-project" >}} ```toml [project] # ... dependencies = [ "tomli (>=2.0.1,<3.0) ; python_version < '3.11'", "pathlib2 (>=2.2,<3.0) ; python_version >= '3.9' and python_version < '4.0'" ] ``` {{< /tab >}} {{< tab tabID="python-restriction-poetry" >}} ```toml [tool.poetry.dependencies] tomli = { version = "^2.0.1", python = "<3.11" } pathlib2 = { version = "^2.2", python = "^3.9" } ``` {{< /tab >}} {{< /tabs >}} ## Using environment markers If you need more complex install conditions for your dependencies, Poetry supports [environment markers](https://www.python.org/dev/peps/pep-0508/#environment-markers): {{< tabs tabTotal="2" tabID1="markers-project" tabID2="markers-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="markers-project" >}} ```toml [project] # ... dependencies = [ "pathlib2 (>=2.2,<3.0) ; python_version <= '3.4' or sys_platform == 'win32'" ] ``` {{< /tab >}} {{< tab tabID="markers-poetry" >}} Use the `markers` property: ```toml [tool.poetry.dependencies] pathlib2 = { version = "^2.2", markers = "python_version <= '3.4' or sys_platform == 'win32'" } ``` {{< /tab >}} {{< /tabs >}} ### `extra` environment marker Poetry populates the `extra` marker with each of the selected extras of the root package. For example, consider the following dependency: ```toml [project.optional-dependencies] paths = [ "pathlib2 (>=2.2,<3.0) ; sys_platform == 'win32'" ] ``` `pathlib2` will be installed when you install your package with `--extras paths` on a `win32` machine. #### Exclusive extras {{% warning %}} The first example will only work completely if you configure Poetry to not re-resolve for installation: ```bash poetry config installer.re-resolve false ``` This was a new feature of Poetry 2.0 and became the default behavior in Poetry 2.3. {{% /warning %}} Keep in mind that all combinations of possible extras available in your project need to be compatible with each other. This means that in order to use differing or incompatible versions across different combinations, you need to make your extra markers *exclusive*. For example, the following installs PyTorch from one source repository with CPU versions when the `cuda` extra is *not* specified, while the other installs from another repository with a separate version set for GPUs when the `cuda` extra *is* specified: ```toml [project] name = "torch-example" requires-python = ">=3.10" dependencies = [ "torch (==2.3.1+cpu) ; extra != 'cuda'", ] [project.optional-dependencies] cuda = [ "torch (==2.3.1+cu118)", ] [tool.poetry] package-mode = false [tool.poetry.dependencies] torch = [ { markers = "extra != 'cuda'", source = "pytorch-cpu"}, { markers = "extra == 'cuda'", source = "pytorch-cuda"}, ] [[tool.poetry.source]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" priority = "explicit" [[tool.poetry.source]] name = "pytorch-cuda" url = "https://download.pytorch.org/whl/cu118" priority = "explicit" ``` For the CPU case, we have to specify `"extra != 'cuda'"` because the version specified is not compatible with the GPU (`cuda`) version. This same logic applies when you want either-or extras: ```toml [project] name = "torch-example" requires-python = ">=3.10" [project.optional-dependencies] cpu = [ "torch (==2.3.1+cpu)", ] cuda = [ "torch (==2.3.1+cu118)", ] [tool.poetry] package-mode = false [tool.poetry.dependencies] torch = [ { markers = "extra == 'cpu' and extra != 'cuda'", source = "pytorch-cpu"}, { markers = "extra == 'cuda' and extra != 'cpu'", source = "pytorch-cuda"}, ] [[tool.poetry.source]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" priority = "explicit" [[tool.poetry.source]] name = "pytorch-cuda" url = "https://download.pytorch.org/whl/cu118" priority = "explicit" ``` ## Multiple constraints dependencies Sometimes, one of your dependencies may have different version ranges depending on the target Python versions. Let's say you have a dependency on the package `foo` which is only compatible with Python 3.6–3.7 up to version 1.9, and compatible with Python 3.8+ from version 2.0: you would declare it like so: {{< tabs tabTotal="2" tabID1="multiple-constraints-project" tabID2="multiple-constraints-poetry" tabName1="[project]" tabName2="[tool.poetry]">}} {{< tab tabID="multiple-constraints-project" >}} ```toml [project] # ... dependencies = [ "foo (<=1.9) ; python_version >= '3.6' and python_version < '3.8'", "foo (>=2.0,<3.0) ; python_version >= '3.8'" ] ``` {{< /tab >}} {{< tab tabID="multiple-constraints-poetry" >}} ```toml [tool.poetry.dependencies] foo = [ {version = "<=1.9", python = ">=3.6,<3.8"}, {version = "^2.0", python = ">=3.8"} ] ``` {{< /tab >}} {{< /tabs >}} {{% note %}} The constraints **must** have different requirements (like `python`) otherwise it will cause an error when resolving dependencies. {{% /note %}} ### Combining git / url / path dependencies with source repositories Direct origin (`git`/ `url`/ `path`) dependencies can satisfy the requirement of a dependency that doesn't explicitly specify a source, even when mutually exclusive markers are used. For instance, in the following example, the url package will also be a valid solution for the second requirement: ```toml foo = [ { platform = "darwin", url = "https://example.com/example-1.0-py3-none-any.whl" }, { platform = "linux", version = "^1.0" }, ] ``` Sometimes you may instead want to use a direct origin dependency for specific conditions (i.e., a compiled package that is not available on PyPI for a certain platform/architecture) while falling back on source repositories in other cases. In this case you should explicitly ask for your dependency to be satisfied by another `source`. For example: ```toml foo = [ { platform = "darwin", url = "https://example.com/foo-1.0.0-py3-none-macosx_11_0_arm64.whl" }, { platform = "linux", version = "^1.0", source = "pypi" }, ] ``` ## Expanded dependency specification syntax In the case of more complex dependency specifications, you may find that you end up with lines which are very long and difficult to read. In these cases, you can shift from using "inline table" syntax to the "standard table" syntax. An example where this might be useful is the following: ```toml [tool.poetry.group.dev.dependencies] black = {version = "19.10b0", allow-prereleases = true, python = "^3.7", markers = "platform_python_implementation == 'CPython'"} ``` As a single line, this is a lot to digest. To make this a bit easier to work with, you can do the following: ```toml [tool.poetry.group.dev.dependencies.black] version = "19.10b0" allow-prereleases = true python = "^3.7" markers = "platform_python_implementation == 'CPython'" ``` The same information is still present, and ends up providing the exact same specification. It's simply split into multiple, slightly more readable, lines. ### Handling of pre-releases Per default, Poetry will prefer stable releases and only choose a pre-release if no stable release satisfies a version constraint. In some cases, this may result in a solution containing pre-releases even if another solution without pre-releases exists. If you want to disallow pre-releases for a specific dependency, you can set `allow-prereleases` to `false`. In this case, dependency resolution will fail if there is no solution without choosing a pre-release. If you want to prefer the latest version of a dependency even if it is a pre-release, you can set `allow-prereleases` to `true` so that Poetry makes no distinction between stable and pre-release versions during dependency resolution. ================================================ FILE: docs/faq.md ================================================ --- title: "FAQ" draft: false type: docs layout: single menu: docs: weight: 110 --- # FAQ ### Why is the dependency resolution process slow? While the dependency resolver at the heart of Poetry is highly optimized and should be fast enough for most cases, with certain sets of dependencies, it can take time to find a valid solution. This is due to the fact that not all libraries on PyPI have properly declared their metadata and, as such, they are not available via the PyPI JSON API. At this point, Poetry has no choice but to download the packages and inspect them to get the necessary information. This is an expensive operation, both in bandwidth and time, which is why it seems this is a long process. At the moment, there is no way around it. However, if you notice that Poetry is downloading many versions of a single package, you can lessen the workload by constraining that one package in your pyproject.toml more narrowly. That way, Poetry does not have to sift through so many versions of it, which may speed up the locking process considerably in some cases. {{% note %}} Once Poetry has cached the releases' information on your machine, the dependency resolution process will be much faster. {{% /note %}} ### What kind of versioning scheme does Poetry use for itself? Poetry uses "major.minor.micro" version identifiers as mentioned in [PEP 440](https://peps.python.org/pep-0440/#final-releases). Version bumps are done similar to Python's versioning: * A major version bump (incrementing the first number) is only done for breaking changes if a deprecation cycle is not possible, and many users have to perform some manual steps to migrate from one version to the next one. * A minor version bump (incrementing the second number) may include new features as well as new deprecations and drop features deprecated in an earlier minor release. * A micro version bump (incrementing the third number) usually only includes bug fixes. Deprecated features will not be dropped in a micro release. ### Why does Poetry not adhere to semantic versioning? Because of its large user base, even small changes not considered relevant by most users can turn out to be a breaking change for some users in hindsight. Sticking to strict [semantic versioning](https://semver.org) and (almost) always bumping the major version instead of the minor version does not seem desirable since the minor version will not carry any meaning anymore. ### Are unbound version constraints a bad idea? A version constraint without an upper bound such as `*` or `>=3.4` will allow updates to any future version of the dependency. This includes major versions breaking backward compatibility. Once a release of your package is published, you cannot tweak its dependencies anymore in case a dependency breaks BC – you have to do a new release but the previous one stays broken. (Users can still work around the broken dependency by restricting it by themselves.) To avoid such issues, you can define an upper bound on your constraints, which you can increase in a new release after testing that your package is compatible with the new major version of your dependency. For example, instead of using `>=3.4` you can use `^3.4` which allows all versions `<4.0`. The `^` operator works very well with libraries following [semantic versioning](https://semver.org). However, when defining an upper bound, users of your package are not able to update a dependency beyond the upper bound even if it does not break anything and is fully compatible with your package. You have to release a new version of your package with an increased upper-bound first. If your package is used as a library in other packages, it might be better to avoid upper bounds and thus unnecessary dependency conflicts (unless you already know for sure that the next release of the dependency will break your package). If your package is used as an application, it might be worth defining an upper bound. ### Is tox supported? **Yes**. Provided that you are using `tox` >= 4, you can use it in combination with the PEP 517 compliant build system provided by Poetry. (With tox 3, you have to set the [isolated build](https://tox.wiki/en/3.27.1/config.html#conf-isolated_build) option.) So, in your `pyproject.toml` file, add this section if it does not already exist: ```toml [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ``` `tox` can be configured in multiple ways. It depends on what should be the code under test and which dependencies should be installed. #### Use case #1 ```ini [tox] [testenv] deps = pytest commands = pytest tests/ --import-mode importlib ``` `tox` will create an `sdist` package of the project and uses `pip` to install it in a fresh environment. Thus, dependencies are resolved by `pip`. #### Use case #2 ```ini [tox] [testenv] allowlist_externals = poetry commands_pre = poetry install --no-root --sync commands = poetry run pytest tests/ --import-mode importlib ``` `tox` will create an `sdist` package of the project and uses `pip` to install it in a fresh environment. Thus, dependencies are resolved by `pip` in the first place. But afterward, we run Poetry, which will install the locked dependencies into the environment. #### Use case #3 ```ini [tox] [testenv] skip_install = true allowlist_externals = poetry commands_pre = poetry install commands = poetry run pytest tests/ --import-mode importlib ``` `tox` will not do any install. Poetry installs all the dependencies and the current package in editable mode. Thus, tests are running against the local files and not the built and installed package. #### Note about credentials Note that `tox` does not forward the environment variables of your current shell session by default. This may cause Poetry to not be able to install dependencies in the `tox` environments if you have configured credentials using the system keyring on Linux systems or using environment variables in general. You can use the `passenv` [configuration option](https://tox.wiki/en/latest/config.html#passenv) to forward the required variables explicitly or `passenv = "*"` to forward all of them. Linux systems may require forwarding the `DBUS_SESSION_BUS_ADDRESS` variable to allow access to the system keyring, though this may vary between desktop environments. Alternatively, you can disable the keyring completely: ```bash poetry config keyring.enabled false ``` Be aware that this will cause Poetry to write passwords to plaintext config files. You will need to set the credentials again after changing this setting. ### Is Nox supported? Use the [`nox-poetry`](https://github.com/cjolowicz/nox-poetry) package to install locked versions of dependencies specified in `poetry.lock` into [Nox](https://nox.thea.codes/en/stable/) sessions. ### I don't want Poetry to manage my virtual environments. Can I disable it? While Poetry automatically creates virtual environments to always work isolated from the global Python installation, there are rare scenarios where the use of a Poetry managed virtual environment is not possible or preferred. In this case, you can disable this feature by setting the `virtualenvs.create` setting to `false`: ```bash poetry config virtualenvs.create false ``` {{% warning %}} The recommended best practice, including when installing an application within a container, is to make use of a virtual environment. This can also be managed by another tool. The Poetry team strongly encourages the use of a virtual environment. {{% /warning %}} ### Why is Poetry telling me that the current project's supported Python range is not compatible with one or more packages' Python requirements? Unlike `pip`, Poetry doesn't resolve for just the Python in the current environment. Instead, it makes sure that a dependency is resolvable within the given Python version range in `pyproject.toml`. Assume you have the following `pyproject.toml`: ```toml [tool.poetry.dependencies] python = "^3.7" ``` This means your project aims to be compatible with any Python version >=3.7,<4.0. Whenever you try to add a dependency whose Python requirement doesn't match the whole range, Poetry will tell you this, e.g.: ``` The current project's supported Python range (>=3.7.0,<4.0.0) is not compatible with some of the required packages Python requirement: - scipy requires Python >=3.7,<3.11, so it will not be installable for Python >=3.11,<4.0.0 ``` Usually you will want to match the supported Python range of your project with the upper bound of the failing dependency. Alternatively, you can tell Poetry to install this dependency [only for a specific range of Python versions]({{< relref "dependency-specification#multiple-constraints-dependencies" >}}), if you know that it's not needed in all versions. If you do not want to set an upper bound in the metadata when building your project, you can omit it in the `project` section and only set it in `tool.poetry.dependencies`: ```toml [project] # ... requires-python = ">=3.7" # used for metadata when building the project [tool.poetry.dependencies] python = ">=3.7,<3.11" # used for locking dependencies ``` ### Why does Poetry enforce PEP 440 versions? This is done to be compliant with the broader Python ecosystem. For example, if Poetry builds a distribution for a project that uses a version that is not valid, according to [PEP 440](https://peps.python.org/pep-0440), third party tools will be unable to parse the version correctly. ### Poetry busts my Docker cache because it requires me to COPY my source files in before installing 3rd party dependencies By default, running `poetry install ...` requires you to have your source files present (both the "root" package and any directory path dependencies you might have). This interacts poorly with Docker's caching mechanisms because any change to a source file will make any layers (subsequent commands in your Dockerfile) re-run. For example, you might have a Dockerfile that looks something like this: ```text FROM python COPY pyproject.toml poetry.lock . COPY src/ ./src RUN pip install poetry && poetry install --only main ``` As soon as *any* source file changes, the cache for the `RUN` layer will be invalidated, which forces all 3rd party dependencies (likely the slowest step out of these) to be installed again if you changed any files in `src/`. To avoid this cache busting you can split this into two steps: 1. Install 3rd party dependencies. 2. Copy over your source code and install just the source code. This might look something like this: ```text FROM python COPY pyproject.toml poetry.lock . RUN pip install poetry && poetry install --only main --no-root --no-directory COPY src/ ./src RUN poetry install --only main ``` The two key options we are using here are `--no-root` (skips installing the project source) and `--no-directory` (skips installing any local directory path dependencies, you can omit this if you don't have any). [More information on the options available for `poetry install`]({{< relref "cli#install" >}}). ### My requests are timing out! Poetry's default HTTP request timeout is 15 seconds, the same as `pip`. Similar to `PIP_REQUESTS_TIMEOUT`, the **experimental** environment variable `POETRY_REQUESTS_TIMEOUT` can be set to alter this value. ### How do I migrate an existing Poetry project using `tools.poetry` section to use the new `project` section (PEP 621)? {{% note %}} Poetry `>=2.0.0` should seamlessly support both `tools.poetry` section only configuration as well using the `project` section. This lets you decide when and if you would like to migrate to using the `project` section as [described by PyPA](https://packaging.python.org/en/latest/specifications/pyproject-toml/#declaring-project-metadata-the-project-table). See documentation on [the `pyproject.toml` file]({{< relref "pyproject" >}}), for information specific to Poetry. {{% /note %}} Due to the nature of this change some manual changes to your `pyproject.toml` file is unavoidable in order start using the `project` section. The following tabs show a transition example. If you wish to retain Poetry's richer [dependency specification]({{< relref "dependency-specification" >}}) syntax it is recommended that you use dynamic dependencies as described in the second tab below. {{< tabs tabTotal="3" tabID1="migrate-pep621-old" tabName1="Original" tabID2="migrate-pep621-new-dynamic" tabName2="Using Dynamic Dependencies" tabID3="migrate-pep621-new-static" tabName3="Using Static Dependencies">}} {{< tab tabID="migrate-pep621-old" >}} ```toml [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Baz Qux "] readme = "README.md" packages = [{ include = "awesome", from = "src" }] include = [{ path = "tests", format = "sdist" }] homepage = "https://python-foobar.org/" repository = "https://github.com/python-foobar/foobar" documentation = "https://python-foobar.org/docs" keywords = ["packaging", "dependency", "foobar"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] [tool.poetry.scripts] foobar = "foobar.console.application:main" [tool.poetry.dependencies] python = "^3.13" httpx = "^0.28.1" [tool.poetry.group.dev.dependencies] pre-commit = ">=2.10" [tool.poetry.group.test.dependencies] pytest = ">=8.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" ``` {{< /tab >}} {{< tab tabID="migrate-pep621-new-static" >}} ```toml [project] name = "foobar" version = "0.1.0" description = "" authors = [ { name = "Baz Qux", email = "baz.qux@example.com" } ] readme = "README.md" requires-python = ">=3.13" keywords = ["packaging", "dependency", "foobar"] # classifiers property is dynamic because we want to create Python classifiers automatically # dependencies are dynamic because we want to keep Poetry's rich dependency definition format dynamic = ["classifiers", "dependencies"] [project.urls] homepage = "https://python-foobar.org/" repository = "https://github.com/python-foobar/foobar" documentation = "https://python-foobar.org/docs" [project.scripts] foobar = "foobar.console.application:main" [tool.poetry] requires-poetry = ">=2.0" packages = [{ include = "foobar", from = "src" }] include = [{ path = "tests", format = "sdist" }] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] [tool.poetry.dependencies] httpx = "^0.28.1" [tool.poetry.group.dev.dependencies] pre-commit = ">=2.10" [tool.poetry.group.test.dependencies] pytest = ">=8.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" ``` {{< /tab >}} {{< tab tabID="migrate-pep621-new-static" >}} ```toml [project] name = "foobar" version = "0.1.0" description = "" authors = [ { name = "Baz Qux", email = "baz.qux@example.com" } ] readme = "README.md" requires-python = ">=3.13" keywords = ["packaging", "dependency", "foobar"] # classifiers property is dynamic because we want to create Python classifiers automatically dynamic = ["classifiers"] dependencies = [ "httpx (>=0.28.1,<0.29.0)" ] [project.urls] homepage = "https://python-foobar.org/" repository = "https://github.com/python-foobar/foobar" documentation = "https://python-foobar.org/docs" [project.scripts] foobar = "foobar.console.application:main" [tool.poetry] requires-poetry = ">=2.0" packages = [{ include = "foobar", from = "src" }] include = [{ path = "tests", format = "sdist" }] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] [tool.poetry.group.dev.dependencies] pre-commit = ">=2.10" [tool.poetry.group.test.dependencies] pytest = ">=8.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" ``` {{< /tab >}} {{< /tabs >}} {{% note %}} - The `classifiers` property is dynamic to allow Poetry to create and manage Python classifiers in accordance with supported Python version. - The `python` dependency, in this example was replaced with `project.requires-python`. However, note that if you need an upper bound on supported Python versions refer to the documentation [here]({{< relref "pyproject#requires-python" >}}). - The [`requires-poetry`]({{< relref "pyproject#requires-poetry" >}}) is added to the `tools.poetry` section. {{% /note %}} ================================================ FILE: docs/libraries.md ================================================ --- title: "Libraries" draft: false type: docs layout: "docs" menu: docs: weight: 20 --- # Libraries This chapter will tell you how to make your library installable through Poetry. ## Versioning Poetry requires [PEP 440](https://peps.python.org/pep-0440)-compliant versions for all projects. While Poetry does not enforce any release convention, it used to encourage the use of [semantic versioning](https://semver.org/) within the scope of [PEP 440](https://peps.python.org/pep-0440/#semantic-versioning) and supports [version constraints]({{< relref "dependency-specification/#caret-requirements" >}}) that are especially suitable for semver. {{% note %}} As an example, `1.0.0-hotfix.1` is not compatible with [PEP 440](https://peps.python.org/pep-0440). You can instead choose to use `1.0.0-post1` or `1.0.0.post1`. {{% /note %}} ## Lock file For your library, you may commit the `poetry.lock` file if you want to. This can help your team to always test against the same dependency versions. However, this lock file will not have any effect on other projects that depend on it. It only has an effect on the main project. If you do not want to commit the lock file and you are using git, add it to the `.gitignore`. ## Packaging Before you can actually publish your library, you will need to package it. You need to define a build-system according to [PEP 517](https://peps.python.org/pep-0517/) in the `pyproject.toml` file: ```toml [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" ``` Then you can package your library by running: ```bash poetry build ``` This command will package your library in two different formats: `sdist` which is the source format, and `wheel` which is a `compiled` package. Poetry will automatically include some license-related files when building a package - in the `.dist-info/licenses` directory when building a `wheel`, and in the root folder when building an `sdist`: - `LICENSE*` - `LICENCE*` - `COPYING*` - `AUTHORS*` - `NOTICE*` - `LICENSES/**/*` You can override this behavior by specifying [`license-files`]({{< relref "pyproject/#license-files" >}}) in the `pyproject.toml` file. ### Alternative build backends If you want to use a different build backend, you can specify it in the `pyproject.toml` file: ```toml [build-system] requires = ["maturin>=0.8.1,<0.9"] build-backend = "maturin" ``` The `poetry build` command will then use the specified build backend to build your package in an isolated environment. Ensure you have specified any additional settings according to the documentation of the build backend you are using. Once building is done, you are ready to publish your library. ## Publishing to PyPI Alright, so now you can publish packages. Poetry will publish to [PyPI](https://pypi.org) by default. Anything that is published to PyPI is available automatically through Poetry. Since [pendulum](https://pypi.org/project/pendulum/) is on PyPI we can depend on it without having to specify any additional repositories. If we wanted to share `poetry-demo` with the Python community, we would publish on PyPI as well. Doing so is really easy. ```bash poetry publish ``` This will package and publish the library to PyPI, on the condition that you are a registered user and you have [configured your credentials]({{< relref "repositories#configuring-credentials" >}}) properly. {{% note %}} The `publish` command does not execute `build` by default. If you want to build and publish your packages together, just pass the `--build` option. {{% /note %}} Once this is done, your library will be available to anyone. ## Publishing to a private repository Sometimes, you may want to keep your library private but also be accessible to your team. In this case, you will need to use a private repository. In order to publish to a private repository, you will need to add it to your global list of repositories. See [Adding a repository]({{< relref "repositories#adding-a-repository" >}}) for more information. Once this is done, you can publish your package to the repository like so: ```bash poetry publish -r my-repository ``` ================================================ FILE: docs/managing-dependencies.md ================================================ --- draft: false layout: single menu: docs: weight: 11 title: Managing dependencies type: docs --- # Managing dependencies Poetry supports specifying main dependencies in the [`project.dependencies`]({{< relref "pyproject#dependencies" >}}) section of your `pyproject.toml` according to PEP 621. For legacy reasons and to define additional information that are only used by Poetry the [`tool.poetry.dependencies`]({{< relref "pyproject#dependencies-and-dependency-groups" >}}) sections can be used. See [Dependency specification]({{< relref "dependency-specification" >}}) for more information. ## Dependency groups Poetry provides a way to **organize** your dependencies by **groups**. The dependencies declared in `project.dependencies` respectively `tool.poetry.dependencies` are part of an implicit `main` group. Those dependencies are required by your project during runtime. Besides the `main` dependencies, you might have dependencies that are only needed to test your project or to build the documentation. To declare a new dependency group, use a `dependency-groups` section according to PEP 735 or a `tool.poetry.group.` section where `` is the name of your dependency group (for instance, `test`): {{< tabs tabTotal="2" tabID1="group-pep735" tabID2="group-poetry" tabName1="[dependency-groups]" tabName2="[tool.poetry]">}} {{< tab tabID="group-pep735" >}} ```toml [dependency-groups] test = [ "pytest (>=6.0.0,<7.0.0)", "pytest-mock", ] ``` {{< /tab >}} {{< tab tabID="group-poetry" >}} ```toml [tool.poetry.group.test.dependencies] pytest = "^6.0.0" pytest-mock = "*" ``` {{< /tab >}} {{< /tabs >}} {{% note %}} All dependencies **must be compatible with each other** across groups since they will be resolved regardless of whether they are required for installation or not (see [Installing group dependencies]({{< relref "#installing-group-dependencies" >}})). Think of dependency groups as **labels** associated with your dependencies: they don't have any bearings on whether their dependencies will be resolved and installed **by default**, they are simply a way to organize the dependencies logically. {{% /note %}} {{% note %}} Dependency groups, other than the implicit `main` group, must only contain dependencies you need in your development process. To declare a set of dependencies, which add additional functionality to the project during runtime, use [extras]({{< relref "pyproject#extras" >}}) instead. {{% /note %}} ### Optional groups A dependency group can be declared as optional. This makes sense when you have a group of dependencies that are only required in a particular environment or for a specific purpose. {{< tabs tabTotal="2" tabID1="group-optional-pep735" tabID2="group-optional-poetry" tabName1="[dependency-groups]" tabName2="[tool.poetry]">}} {{< tab tabID="group-optional-pep735" >}} ```toml [dependency-groups] docs = [ "mkdocs", ] [tool.poetry.group.docs] optional = true ``` {{< /tab >}} {{< tab tabID="group-optional-poetry" >}} ```toml [tool.poetry.group.docs] optional = true [tool.poetry.group.docs.dependencies] mkdocs = "*" ``` {{< /tab >}} {{< /tabs >}} Optional groups can be installed in addition to the **default** dependencies by using the `--with` option of the [`install`]({{< relref "cli#install" >}}) command. ```bash poetry install --with docs ``` {{% warning %}} Optional group dependencies will **still** be resolved alongside other dependencies, so special care should be taken to ensure they are compatible with each other. {{% /warning %}} ### Including dependencies from other groups You can include dependencies from one group in another group. This is useful when you want to aggregate dependencies from multiple groups into a single group. {{< tabs tabTotal="2" tabID1="group-include-pep735" tabID2="group-include-poetry" tabName1="[dependency-groups]" tabName2="[tool.poetry]">}} {{< tab tabID="group-include-pep735" >}} ```toml [dependency-groups] test = [ "pytest (>=8.0.0,<9.0.0)", ] lint = [ "ruff (>=0.11.0,<0.12.0)", ] dev = [ { include-group = "test" }, { include-group = "lint" }, "tox", ] ``` {{< /tab >}} {{< tab tabID="group-include-poetry" >}} ```toml [tool.poetry.group.test.dependencies] pytest = "^8.0.0" [tool.poetry.group.lint.dependencies] ruff = "^0.11" [tool.poetry.group.dev] include-groups = [ "test", "lint", ] [tool.poetry.group.dev.dependencies] tox = "*" ``` {{< /tab >}} {{< /tabs >}} In this example, the `dev` group includes all dependencies from the `test` and `lint` groups. ### Adding a dependency to a group The [`add`]({{< relref "cli#add" >}}) command is the preferred way to add dependencies to a group. This is done by using the `--group (-G)` option. ```bash poetry add pytest --group test ``` If the group does not already exist, it will be created automatically. ### Installing group dependencies **By default**, dependencies across **all non-optional groups** will be installed when executing `poetry install`. {{% note %}} The default set of dependencies for a project includes the implicit `main` group as well as all groups that are not explicitly marked as an [optional group]({{< relref "#optional-groups" >}}). {{% /note %}} You can **exclude** one or more groups with the `--without` option: ```bash poetry install --without test,docs ``` You can also opt in [optional groups]({{< relref "#optional-groups" >}}) by using the `--with` option: ```bash poetry install --with docs ``` {{% warning %}} When used together, `--without` takes precedence over `--with`. For example, the following command will only install the dependencies specified in the optional `test` group. ```bash poetry install --with test,docs --without docs ``` {{% /warning %}} Finally, in some case you might want to install **only specific groups** of dependencies without installing the default set of dependencies. For that purpose, you can use the `--only` option. ```bash poetry install --only docs ``` {{% note %}} If you only want to install the project's runtime dependencies, you can do so with the `--only main` notation: ```bash poetry install --only main ``` {{% /note %}} {{% note %}} If you want to install the project root, and no other dependencies, you can use the `--only-root` option. ```bash poetry install --only-root ``` {{% /note %}} ### Removing dependencies from a group The [`remove`]({{< relref "cli#remove" >}}) command supports a `--group` option to remove packages from a specific group: ```bash poetry remove mkdocs --group docs ``` ## Synchronizing dependencies Poetry supports what's called dependency synchronization. Dependency synchronization ensures that the locked dependencies in the `poetry.lock` file are the only ones present in the environment, removing anything that's not necessary. This is done by using the `sync` command: ```bash poetry sync ``` The `sync` command can be combined with any [dependency groups]({{< relref "#dependency-groups" >}}) related options to synchronize the environment with specific groups. Note that extras are separate. Any extras not selected for install are always removed. ```bash poetry sync --without dev poetry sync --with docs poetry sync --only dev ``` ## Layering optional groups When using the `install` command without the `--sync` option, you can install any subset of optional groups without removing those that are already installed. This is very useful, for example, in multi-stage Docker builds, where you run `poetry install` multiple times in different build stages. ================================================ FILE: docs/managing-environments.md ================================================ --- title: "Managing environments" draft: false type: docs layout: "docs" menu: docs: weight: 60 --- # Managing environments Poetry makes project environment isolation one of its core features. What this means is that it will always work isolated from your global Python installation. To achieve this, it will first check if it's currently running inside a virtual environment. If it is, it will use it directly without creating a new one. But if it's not, it will use one that it has already created or create a brand new one for you. By default, Poetry will try to use the Python version used during Poetry's installation to create the virtual environment for the current project. However, for various reasons, this Python version might not be compatible with the `python` range supported by the project. In this case, Poetry will try to find one that is and use it. If it's unable to do so then you will be prompted to activate one explicitly, see [Switching environments](#switching-between-environments). {{% note %}} If you use a tool like [pyenv](https://github.com/pyenv/pyenv) to manage different Python versions, you can switch the current `python` of your shell and Poetry will use it to create the new environment. For instance, if your project requires a newer Python than is available with your system, a standard workflow would be: ```bash pyenv install 3.9.8 pyenv local 3.9.8 # Activate Python 3.9 for the current project poetry install ``` {{% /note %}} {{% note %}} Since version 1.2, Poetry no longer supports managing environments for Python 2.7. {{% /note %}} ## Switching between environments Sometimes this might not be feasible for your system, especially Windows where `pyenv` is not available, or you simply prefer to have a more explicit control over your environment. For this specific purpose, you can use the `env use` command to tell Poetry which Python version to use for the current project. ```bash poetry env use /full/path/to/python ``` If you have the python executable in your `PATH` you can use it: ```bash poetry env use python3.7 ``` You can even just use the minor Python version in this case: ```bash poetry env use 3.7 ``` If you want to disable the explicitly activated virtual environment, you can use the special `system` Python version to retrieve the default behavior: ```bash poetry env use system ``` ## Activating the environment {{% note %}} Looking for `poetry shell`? It was moved to a plugin: [`poetry-plugin-shell`](https://github.com/python-poetry/poetry-plugin-shell) {{% /note %}} The `poetry env activate` command prints the activate command of the virtual environment to the console. You can run the output command manually or feed it to the eval command of your shell to activate the environment. This way you won't leave the current shell. {{< tabs tabTotal="3" tabID1="bash-csh-zsh" tabID2="fish" tabID3="powershell" tabName1="Bash/Zsh/Csh" tabName2="Fish" tabName3="Powershell" >}} {{< tab tabID="bash-csh-zsh" >}} ```bash $ eval $(poetry env activate) (test-project-for-test) $ # Virtualenv entered ``` {{< /tab >}} {{< tab tabID="fish" >}} ```bash $ eval (poetry env activate) (test-project-for-test) $ # Virtualenv entered ``` {{< /tab >}} {{< tab tabID="powershell" >}} ```ps1 PS1> Invoke-Expression (poetry env activate) (test-project-for-test) PS1> # Virtualenv entered ``` {{< /tab >}} {{< /tabs >}} ## Displaying the environment information If you want to get basic information about the currently activated virtual environment, you can use the `env info` command: ```bash poetry env info ``` will output something similar to this: ```text Virtualenv Python: 3.7.1 Implementation: CPython Path: /path/to/poetry/cache/virtualenvs/test-O3eWbxRl-py3.7 Valid: True Base Platform: darwin OS: posix Python: /path/to/main/python ``` If you only want to know the path to the virtual environment, you can pass the `--path` option to `env info`: ```bash poetry env info --path ``` If you only want to know the path to the python executable (useful for running mypy from a global environment without installing it in the virtual environment), you can pass the `--executable` option to `env info`: ```bash poetry env info --executable ``` ## Listing the environments associated with the project You can also list all the virtual environments associated with the current project with the `env list` command: ```bash poetry env list ``` will output something like the following: ```text test-O3eWbxRl-py3.6 test-O3eWbxRl-py3.7 (Activated) ``` You can pass the option `--full-path` to display the full path to the environments: ```bash poetry env list --full-path ``` ## Deleting the environments Finally, you can delete existing virtual environments by using `env remove`: ```bash poetry env remove /full/path/to/python poetry env remove python3.7 poetry env remove 3.7 poetry env remove test-O3eWbxRl-py3.7 ``` You can delete more than one environment at a time. ```bash poetry env remove python3.6 python3.7 python3.8 ``` Use the `--all` option to delete all virtual environments at once. ```bash poetry env remove --all ``` If you remove the currently activated virtual environment, it will be automatically deactivated. {{% note %}} If you use the [`virtualenvs.in-project`]({{< relref "configuration#virtualenvsin-project" >}}) configuration, you can simply use the command as shown below. ```bash poetry env remove ``` {{% /note %}} ================================================ FILE: docs/plugins.md ================================================ --- title: "Plugins" draft: false type: docs layout: single menu: docs: weight: 80 --- # Plugins Poetry supports using and building plugins if you wish to alter or expand Poetry's functionality with your own. For example if your environment poses special requirements on the behaviour of Poetry which do not apply to the majority of its users or if you wish to accomplish something with Poetry in a way that is not desired by most users. In these cases you could consider creating a plugin to handle your specific logic. ## Creating a plugin A plugin is a regular Python package which ships its code as part of the package and may also depend on further packages. ### Plugin package The plugin package must depend on Poetry and declare a proper [plugin]({{< relref "pyproject#plugins" >}}) in the `pyproject.toml` file. ```toml [project] name = "my-poetry-plugin" version = "1.0.0" # ... requires-python = ">=3.7" dependencies = [ "poetry (>=1.2,<2.0)", ] [project.entry-points."poetry.plugin"] demo = "poetry_demo_plugin.plugin:MyPlugin" ``` ### Generic plugins Every plugin has to supply a class which implements the `poetry.plugins.Plugin` interface. The `activate()` method of the plugin is called after the plugin is loaded and receives an instance of `Poetry` as well as an instance of `cleo.io.io.IO`. Using these two objects all configuration can be read and all public internal objects and state can be manipulated as desired. Example: ```python from cleo.io.io import IO from poetry.plugins.plugin import Plugin from poetry.poetry import Poetry class MyPlugin(Plugin): def activate(self, poetry: Poetry, io: IO): io.write_line("Setting readme") poetry.package.readme = "README.md" ... ``` ### Application plugins If you want to add commands or options to the `poetry` script you need to create an application plugin which implements the `poetry.plugins.ApplicationPlugin` interface. The `activate()` method of the application plugin is called after the plugin is loaded and receives an instance of `poetry.console.Application`. ```python from cleo.commands.command import Command from poetry.plugins.application_plugin import ApplicationPlugin class CustomCommand(Command): name = "my-command" def handle(self) -> int: self.line("My command") return 0 def factory(): return CustomCommand() class MyApplicationPlugin(ApplicationPlugin): def activate(self, application): application.command_loader.register_factory("my-command", factory) ``` {{% note %}} It's possible to do the following to register the command: ```python application.add(MyCommand()) ``` However, it is **strongly** recommended to register a new factory in the command loader to defer the loading of the command when it's actually called. This will help keep the performances of Poetry good. {{% /note %}} The plugin also must be declared in the `pyproject.toml` file of the plugin package as a `poetry.application.plugin` plugin: ```toml [tool.poetry.plugins."poetry.application.plugin"] foo-command = "poetry_demo_plugin.plugin:MyApplicationPlugin" ``` {{% warning %}} A plugin **must not** remove or modify in any way the core commands of Poetry. {{% /warning %}} ### Event handler Plugins can also listen to specific events and act on them if necessary. These events are fired by [Cleo](https://github.com/python-poetry/cleo) and are accessible from the `cleo.events.console_events` module. - `COMMAND`: this event allows attaching listeners before any command is executed. - `SIGNAL`: this event allows some actions to be performed after the command execution is interrupted. - `TERMINATE`: this event allows listeners to be attached after the command. - `ERROR`: this event occurs when an uncaught exception is raised. Let's see how to implement an application event handler. For this example we will see how to load environment variables from a `.env` file before executing a command. ```python from cleo.events.console_events import COMMAND from cleo.events.console_command_event import ConsoleCommandEvent from cleo.events.event_dispatcher import EventDispatcher from dotenv import load_dotenv from poetry.console.application import Application from poetry.console.commands.env_command import EnvCommand from poetry.plugins.application_plugin import ApplicationPlugin class MyApplicationPlugin(ApplicationPlugin): def activate(self, application: Application): application.event_dispatcher.add_listener( COMMAND, self.load_dotenv ) def load_dotenv( self, event: ConsoleCommandEvent, event_name: str, dispatcher: EventDispatcher ) -> None: command = event.command if not isinstance(command, EnvCommand): return io = event.io if io.is_debug(): io.write_line( "Loading environment variables." ) load_dotenv() ``` ## Using plugins Installed plugin packages are automatically loaded when Poetry starts up. You have multiple ways to install plugins for Poetry ### With `pipx inject` If you used `pipx` to install Poetry you can add the plugin packages via the `pipx inject` command. ```shell pipx inject poetry poetry-plugin ``` If you want to uninstall a plugin, you can run: ```shell pipx uninject poetry poetry-plugin # For pipx versions >= 1.2.0 pipx runpip poetry uninstall poetry-plugin # For pipx versions < 1.2.0 ``` ### With `pip` The `pip` binary in Poetry's virtual environment can also be used to install and remove plugins. The environment variable `$POETRY_HOME` here is used to represent the path to the virtual environment. The [installation instructions](/docs/) can be referenced if you are not sure where Poetry has been installed. To add a plugin, you can use `pip install`: ```shell $POETRY_HOME/bin/pip install --user poetry-plugin ``` If you want to uninstall a plugin, you can run: ```shell $POETRY_HOME/bin/pip uninstall poetry-plugin ``` ### The `self add` command {{% warning %}} Especially on Windows, `self add` and `self remove` may be problematic so that other methods should be preferred. {{% /warning %}} ```bash poetry self add poetry-plugin ``` The `self add` command will ensure that the plugin is compatible with the current version of Poetry and install the needed packages for the plugin to work. The package specification formats supported by the `self add` command are the same as the ones supported by the [`add` command]({{< relref "cli#add" >}}). If you no longer need a plugin and want to uninstall it, you can use the `self remove` command. ```shell poetry self remove poetry-plugin ``` You can also list all currently installed plugins by running: ```shell poetry self show plugins ``` ### Project plugins You can also specify that a plugin is required for your project in the `tool.poetry.requires-plugins` section of the pyproject.toml file: ```toml [tool.poetry.requires-plugins] my-application-plugin = ">1.0" custom-plugin = {path = "custom_plugin", develop = true} ``` If the plugin is not installed in Poetry's own environment when running `poetry install`, it will be installed only for the current project under `.poetry/plugins` in the project's directory. The syntax to specify `plugins` is the same as for [dependencies]({{< relref "managing-dependencies" >}}). Plugins can be installed in editable mode using path dependencies with `develop = true`, which is useful for plugin development. {{% warning %}} You can even overwrite a plugin in Poetry's own environment with another version. However, if a plugin's dependencies are not compatible with packages in Poetry's own environment, installation will fail. {{% /warning %}} ## Maintaining a plugin When writing a plugin, you will probably access internals of Poetry, since there is no stable public API. Although we try our best to deprecate methods first, before removing them, sometimes the signature of an internal method has to be changed. As the author of a plugin, you are probably testing your plugin against the latest release of Poetry. Additionally, you should consider testing against the latest release branch and the main branch of Poetry and schedule a CI job that runs regularly even if you did not make any changes to your plugin. This way, you will notice internal changes that break your plugin immediately and can prepare for the next Poetry release. ================================================ FILE: docs/pre-commit-hooks.md ================================================ --- title: "pre-commit hooks" draft: false type: docs layout: single menu: docs: weight: 120 --- # pre-commit hooks pre-commit is a framework for building and running [git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). See the official documentation for more information: [pre-commit.com](https://pre-commit.com/) This document provides a list of available pre-commit hooks provided by Poetry. {{% note %}} If you specify the `args:` for a hook in your `.pre-commit-config.yaml`, the defaults are overwritten. You must fully specify all arguments for your hook if you make use of `args:`. {{% /note %}} {{% note %}} If the `pyproject.toml` file is not in the root directory, you can specify `args: ["-C", "./subdirectory"]`. {{% /note %}} ## poetry-check The `poetry-check` hook calls the `poetry check` command to make sure the poetry configuration does not get committed in a broken state. ### Arguments The hook takes the same arguments as the poetry command. For more information see the [check command]({{< relref "cli#check" >}}). ## poetry-lock The `poetry-lock` hook calls the `poetry lock` command to make sure the lock file is up-to-date when committing changes. ### Arguments The hook takes the same arguments as the poetry command. For more information see the [lock command]({{< relref "cli#lock" >}}). ## poetry-export The `poetry-export` hook calls the `poetry export` command to sync your `requirements.txt` file with your current dependencies. {{% warning %}} This hook is provided by the [Export Poetry Plugin](https://github.com/python-poetry/poetry-plugin-export). {{% /warning %}} {{% note %}} It is recommended to run the [`poetry-lock`](#poetry-lock) hook or [`poetry-check`](#poetry-check) with argument `--lock` prior to this one. {{% /note %}} ### Arguments The hook takes the same arguments as the poetry command. For more information, see the [export command]({{< relref "cli#export" >}}). The default arguments are `args: ["-f", "requirements.txt", "-o", "requirements.txt"]`, which will create/update the `requirements.txt` file in the current working directory. You may add `verbose: true` in your `.pre-commit-config.yaml` in order to output to the console: ```yaml hooks: - id: poetry-export args: ["-f", "requirements.txt"] verbose: true ``` Also, `--dev` can be added to `args` to write dev-dependencies to `requirements.txt`: ```yaml hooks: - id: poetry-export args: ["--dev", "-f", "requirements.txt", "-o", "requirements.txt"] ``` ## poetry-install The `poetry-install` hook calls the `poetry install` command to make sure all locked packages are installed. In order to install this hook, you either need to specify `default_install_hook_types`, or you have to install it via `pre-commit install --install-hooks -t post-checkout -t post-merge`. ### Arguments The hook takes the same arguments as the poetry command. For more information, see the [install command]({{< relref "cli#install" >}}). ## Usage For more information on how to use pre-commit, please see the [official documentation](https://pre-commit.com/). A minimalistic `.pre-commit-config.yaml` example: ```yaml repos: - repo: https://github.com/python-poetry/poetry rev: '' # add version here hooks: - id: poetry-check - id: poetry-lock - id: poetry-export - id: poetry-install ``` A `.pre-commit-config.yaml` example for a monorepo setup or if the `pyproject.toml` file is not in the root directory: ```yaml repos: - repo: https://github.com/python-poetry/poetry rev: '' # add version here hooks: - id: poetry-check args: ["-C", "./subdirectory"] - id: poetry-lock args: ["-C", "./subdirectory"] - id: poetry-export args: ["-C", "./subdirectory", "-f", "requirements.txt", "-o", "./subdirectory/requirements.txt"] - id: poetry-install args: ["-C", "./subdirectory"] ``` ## FAQ ### Why does `pre-commit autoupdate` not update to the latest version? `pre-commit autoupdate` updates the `rev` for each repository defined in your `.pre-commit-config.yaml` to the latest available tag in the default branch. Poetry follows a branching strategy where the default branch is the active development branch, and fixes get backported to stable branches. New tags are assigned in these stable branches. `pre-commit` does not support such a branching strategy and has decided to not implement an option, either on the [user's side](https://github.com/pre-commit/pre-commit/issues/2512) or the [hook author's side](https://github.com/pre-commit/pre-commit/issues/2508), to define a branch for looking up the latest available tag. Thus, `pre-commit autoupdate` is not usable for the hooks described here. You can avoid changing the `rev` to an unexpected value by using the `--repo` parameter (may be specified multiple times), to explicitly list repositories that should be updated. An option to explicitly exclude repositories [will not be implemented](https://github.com/pre-commit/pre-commit/issues/1959) into `pre-commit`. ### Are there any alternatives to `pre-commit autoupdate`? You may use [pre-commit-update](https://pypi.org/project/pre-commit-update/) as an alternative to `pre-commit autoupdate`. Since `pre-commit-update` can be used as a pre-commit hook itself, the easiest way to make use of it would be to include it inside `.pre-commit-config.yaml`: ```yaml repos: - repo: https://gitlab.com/vojko.pribudic.foss/pre-commit-update rev: v0.5.1post1 hooks: - id: pre-commit-update - repo: https://github.com/python-poetry/poetry rev: 1.8.3 hooks: - id: poetry-check - id: poetry-lock - id: poetry-export - id: poetry-install ``` Your `.pre-commit-config.yaml` repos will be checked and updated every time pre-commit hooks run. For more advanced configuration, please check the `pre-commit-update` documentation. ================================================ FILE: docs/pyproject.md ================================================ --- title: "The pyproject.toml file" draft: false type: docs layout: single menu: docs: weight: 90 --- # The `pyproject.toml` file In package mode, the only required fields are `name` and `version` (either in the `project` section or in the `tool.poetry` section). Other fields are optional. In non-package mode, the `name` and `version` fields are required if using the `project` section. {{% note %}} Run `poetry check` to print warnings about deprecated fields. {{% /note %}} ## The `project` section The `project` section of the `pyproject.toml` file according to the [specification of the PyPA](https://packaging.python.org/en/latest/specifications/pyproject-toml/#declaring-project-metadata-the-project-table). ### name The name of the package. **Always required when the `project` section is specified** This should be a valid name as defined by [PEP 508](https://peps.python.org/pep-0508/#names). ```toml [project] name = "my-package" ``` ### version The version of the package. **Always required when the `project` section is specified** This should be a valid [PEP 440](https://peps.python.org/pep-0440/) string. ```toml [project] # ... version = "0.1.0" ``` If you want to set the version dynamically via `poetry build --local-version` or you are using a plugin, which sets the version dynamically, you should add `version` to dynamic and define the base version in the `tool.poetry` section, for example: ```toml [project] name = "my-package" dynamic = [ "version" ] [tool.poetry] version = "1.0" # base version ``` ### description A short description of the package. ```toml [project] # ... description = "A short description of the package." ``` ### license An [SPDX expression](https://packaging.python.org/en/latest/glossary/#term-License-Expression) representing the license of the package. The recommended notation for the most common licenses is (alphabetical): * Apache-2.0 * BSD-2-Clause * BSD-3-Clause * BSD-4-Clause * GPL-2.0-only * GPL-2.0-or-later * GPL-3.0-only * GPL-3.0-or-later * LGPL-2.1-only * LGPL-2.1-or-later * LGPL-3.0-only * LGPL-3.0-or-later * MIT Optional, but it is highly recommended to supply this. More identifiers are listed at the [SPDX Open Source License Registry](https://spdx.org/licenses/). ```toml [project] # ... license = "MIT" ``` {{% warning %}} Specifying license as a table, e.g. `{ text = "MIT" }` is deprecated. If you used to specify a license file, e.g. `{ file = "LICENSE" }`, use `license-files` instead. {{% /warning %}} ### license-files A list of glob patterns that match the license files of the package relative to the root of the project source tree. ```toml [project] # ... license-files = [ "*-LICENSE", "CONTRIBUTORS", "MY-SPECIAL-LICENSE-DIR/**/*" ] ``` By default, Poetry will include the following files: - `LICENSE*` - `LICENCE*` - `COPYING*` - `AUTHORS*` - `NOTICE*` - `LICENSES/**/*` {{% note %}} The default applies only if the `license-files` field is not specified. Specifying an empty list results in no license files being included. {{% /note %}} ### readme A path to the README file or the content. ```toml [project] # ... readme = "README.md" ``` {{% note %}} If you want to define multiple README files, you have to add `readme` to `dynamic` and define them in the `tool.poetry` section. {{% /note %}} ```toml [project] # ... dynamic = [ "readme" ] [tool.poetry] # ... readme = ["docs/README1.md", "docs/README2.md"] ``` ### requires-python The Python version requirements of the project. ```toml [project] # ... requires-python = ">=3.8" ``` {{% note %}} If you need an upper bound for locking, but do not want to define an upper bound in your package metadata, you can omit the upper bound in the `requires-python` field and add it in the `tool.poetry.dependencies` section. {{% /note %}} ```toml [project] # ... requires-python = ">=3.8" [tool.poetry.dependencies] python = ">=3.8,<4.0" ``` ### authors The authors of the package. This is a list of authors and should contain at least one author. ```toml [project] # ... authors = [ { name = "Sébastien Eustace", email = "sebastien@eustace.io" }, ] ``` ### maintainers The maintainers of the package. This is a list of maintainers and should be distinct from authors. ```toml [project] # ... maintainers = [ { name = "John Smith", email = "johnsmith@example.org" }, { name = "Jane Smith", email = "janesmith@example.org" }, ] ``` ### keywords A list of keywords that the package is related to. ```toml [project] # ... keywords = [ "packaging", "poetry" ] ``` ### classifiers A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project. ```toml [project] # ... classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] ``` {{% warning %}} Note that suitable classifiers based on your `python` requirement are **not** automatically added for you if you define classifiers statically in the `project` section. If you want to enrich classifiers automatically, you should add `classifiers` to `dynamic` and use the `tool.poetry` section instead. {{% /warning %}} ```toml [project] # ... dynamic = [ "classifiers" ] [tool.poetry] # ... classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] ``` ### urls The URLs of the project. ```toml [project.urls] homepage = "https://python-poetry.org/" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs/" "Bug Tracker" = "https://github.com/python-poetry/poetry/issues" ``` If you publish your package on PyPI, they will appear in the `Project Links` section. ### scripts This section describes the console scripts that will be installed when installing the package. ```toml [project.scripts] my_package_cli = 'my_package.console:run' ``` Here, we will have the `my_package_cli` script installed which will execute the `run` function in the `console` module in the `my_package` package. {{% note %}} When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. {{% /note %}} {{% note %}} To include a file as a script, use [`tool.poetry.scripts`]({{< relref "#scripts-1" >}}) instead. {{% /note %}} ### gui-scripts This section describes the GUI scripts that will be installed when installing the package. ```toml [project.gui-scripts] my_package_gui = 'my_package.gui:run' ``` Here, we will have the `my_package_gui` script installed which will execute the `run` function in the `gui` module in the `my_package` package. {{% note %}} When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. {{% /note %}} ### entry-points Entry points can be used to define plugins for your package. Poetry supports arbitrary plugins, which are exposed as the ecosystem-standard [entry points](https://packaging.python.org/en/latest/specifications/entry-points/) and discoverable using `importlib.metadata`. This is similar to (and compatible with) the entry points feature of `setuptools`. The syntax for registering a plugin is: ```toml [project.entry-points] # Optional super table [project.entry-points."A"] B = "C:D" ``` Which are: - `A` - type of the plugin, for example `poetry.plugin` or `flake8.extension` - `B` - name of the plugin - `C` - python module import path - `D` - the entry point of the plugin (a function or class) Example (from [`poetry-plugin-export`](http://github.com/python-poetry/poetry-plugin-export)): ```toml [project.entry-points."poetry.application.plugin"] export = "poetry_plugin_export.plugins:ExportApplicationPlugin" ``` ### dependencies The `dependencies` of the project. ```toml [project] # ... dependencies = [ "requests>=2.13.0", ] ``` These are the dependencies that will be declared when building an sdist or a wheel. See [Dependency specification]({{< relref "dependency-specification" >}}) for more information about the relation between `project.dependencies` and `tool.poetry.dependencies`. ### optional-dependencies The optional dependencies of the project (also known as extras). ```toml [project.optional-dependencies] mysql = [ "mysqlclient>=1.3,<2.0" ] pgsql = [ "psycopg2>=2.9,<3.0" ] databases = [ "mysqlclient>=1.3,<2.0", "psycopg2>=2.9,<3.0" ] ``` {{% note %}} You can enrich optional dependencies for locking in the `tool.poetry` section analogous to `dependencies`. {{% /note %}} ## The `tool.poetry` section The `tool.poetry` section of the `pyproject.toml` file is composed of multiple sections. ### package-mode Whether Poetry operates in package mode (default) or not. See [basic usage]({{< relref "basic-usage#operating-modes" >}}) for more information. ```toml [tool.poetry] # ... package-mode = false ``` ### name **Deprecated**: Use `project.name` instead. The name of the package. **Required in package mode if not defined in the project section** This should be a valid name as defined by [PEP 508](https://peps.python.org/pep-0508/#names). ```toml [tool.poetry] name = "my-package" ``` ### version {{% note %}} If you do not want to set the version dynamically via `poetry build --local-version` and you are not using a plugin, which sets the version dynamically, prefer `project.version` over this setting. {{% /note %}} The version of the package. **Required in package mode if not defined in the project section** This should be a valid [PEP 440](https://peps.python.org/pep-0440/) string. ```toml [tool.poetry] # ... version = "0.1.0" ``` {{% note %}} If you would like to use semantic versioning for your project, please see [here]({{< relref "libraries#versioning" >}}). {{% /note %}} ### description **Deprecated**: Use `project.description` instead. A short description of the package. ```toml [tool.poetry] # ... description = "A short description of the package." ``` ### license **Deprecated**: Use `project.license` instead. The license of the package. The recommended notation for the most common licenses is (alphabetical): * Apache-2.0 * BSD-2-Clause * BSD-3-Clause * BSD-4-Clause * GPL-2.0-only * GPL-2.0-or-later * GPL-3.0-only * GPL-3.0-or-later * LGPL-2.1-only * LGPL-2.1-or-later * LGPL-3.0-only * LGPL-3.0-or-later * MIT Optional, but it is highly recommended to supply this. More identifiers are listed at the [SPDX Open Source License Registry](https://spdx.org/licenses/). ```toml [tool.poetry] # ... license = "MIT" ``` ### authors **Deprecated**: Use `project.authors` instead. The authors of the package. This is a list of authors and should contain at least one author. Authors must be in the form `name `. ```toml [tool.poetry] # ... authors = [ "Sébastien Eustace ", ] ``` ### maintainers **Deprecated**: Use `project.maintainers` instead. The maintainers of the package. This is a list of maintainers and should be distinct from authors. Maintainers may contain an email and be in the form `name `. ```toml [tool.poetry] # ... maintainers = [ "John Smith ", "Jane Smith ", ] ``` ### readme {{% note %}} If you do not want to set multiple README files, prefer `project.readme` over this setting. {{% /note %}} A path, or list of paths corresponding to the README file(s) of the package. The file(s) can be of any format, but if you intend to publish to PyPI keep the [recommendations for a PyPI-friendly README]( https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/) in mind. README paths are implicitly relative to `pyproject.toml`. {{% note %}} Whether paths are case-sensitive follows platform defaults, but it is recommended to keep cases. To be specific, you can set `readme = "rEaDmE.mD"` for `README.md` on macOS and Windows, but Linux users can't `poetry install` after cloning your repo. This is because macOS and Windows are case-insensitive and case-preserving. {{% /note %}} The contents of the README file(s) are used to populate the [Description field](https://packaging.python.org/en/latest/specifications/core-metadata/#description-optional) of your distribution's metadata (similar to `long_description` in setuptools). When multiple files are specified they are concatenated with newlines. ```toml [tool.poetry] # ... readme = "README.md" ``` ```toml [tool.poetry] # ... readme = ["docs/README1.md", "docs/README2.md"] ``` ### homepage **Deprecated**: Use `project.urls` instead. A URL to the website of the project. ```toml [tool.poetry] # ... homepage = "https://python-poetry.org/" ``` ### repository **Deprecated**: Use `project.urls` instead. A URL to the repository of the project. ```toml [tool.poetry] # ... repository = "https://github.com/python-poetry/poetry" ``` ### documentation **Deprecated**: Use `project.urls` instead. A URL to the documentation of the project. ```toml [tool.poetry] # ... documentation = "https://python-poetry.org/docs/" ``` ### keywords **Deprecated**: Use `project.keywords` instead. A list of keywords that the package is related to. ```toml [tool.poetry] # ... keywords = ["packaging", "poetry"] ``` ### classifiers A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project. ```toml [tool.poetry] # ... classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] ``` {{% note %}} Note that Python classifiers are automatically added for you and are determined by your `python` requirement. If you do not want Poetry to automatically add suitable classifiers based on the `python` requirement, use `project.classifiers` instead of this setting. {{% /note %}} ### packages A list of packages and modules to include in the final distribution. If packages are not automatically detected, you can specify the packages you want to include in the final distribution. {{% note %}} Poetry automatically detects a single **module** or **package** whose name matches the [normalized](https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization) project name with `-` replaced with `_`. The detected module or package must be located either: - at the same level as the `pyproject.toml` file (flat layout), or - inside a `src/` directory (src layout). {{% /note %}} ```toml [tool.poetry] # ... packages = [ { include = "my_package" }, { include = "extra_package/**/*.py" }, ] ``` If your package is stored inside a "lib" directory, you must specify it: ```toml [tool.poetry] # ... packages = [ { include = "my_package", from = "lib" }, ] ``` The `to` parameter is designed to specify the relative destination path where the package will be located upon installation. This allows for greater control over the organization of packages within your project's structure. ```toml [tool.poetry] # ... packages = [ { include = "my_package", from = "lib", to = "target_package" }, ] ``` If you want to restrict a package to a specific build format, you can specify it by using `format`: ```toml [tool.poetry] # ... packages = [ { include = "my_package" }, { include = "my_other_package", format = "sdist" }, ] ``` From now on, only the `sdist` build archive will include the `my_other_package` package. {{% note %}} Using `packages` disables the package auto-detection feature meaning you have to **explicitly** specify the "default" package. For instance, if you have a package named `my_package` and you want to also include another package named `extra_package`, you will need to specify `my_package` explicitly: ```toml [tool.poetry] # ... packages = [ { include = "my_package" }, { include = "extra_package" }, ] ``` {{% /note %}} {{% note %}} Poetry is clever enough to detect Python subpackages. Thus, you only have to specify the directory where your root package resides. {{% /note %}} ### exclude and include {{% note %}} If you just want to include a package or module, which is not picked up automatically, use [packages]({{< relref "#packages" >}}) instead of `include`. {{% /note %}} A list of patterns that will be excluded or included in the final package. ```toml [tool.poetry] # ... exclude = ["my_package/excluded.py"] include = ["CHANGELOG.md"] ``` You can explicitly specify to Poetry that a set of globs should be ignored or included for the purposes of packaging. The globs specified in the exclude field identify a set of files that are not included when a package is built. `include` has priority over `exclude`. You can also specify the formats for which these patterns have to be included, as shown here: ```toml [tool.poetry] # ... include = [ { path = "tests", format = "sdist" }, { path = "my_package/for_sdist_and_wheel.txt", format = ["sdist", "wheel"] } ] ``` If no format is specified, `include` defaults to only `sdist`. In contrast, `exclude` defaults to both `sdist` and `wheel`. {{% warning %}} When a wheel is installed, its includes are unpacked straight into the `site-packages` directory. Pay attention to include top level files and directories with common names like `CHANGELOG.md`, `LICENSE`, `tests` or `docs` only in sdists and **not** in wheels. {{% /warning %}} If a VCS is being used for a package, the exclude field will be seeded with the VCS’ ignore settings (`.gitignore` for git, for example). {{% note %}} VCS ignore settings can be negated by adding entries in `include`; be sure to explicitly set the `format` as above. {{% /note %}} ### dependencies and dependency groups Poetry is configured to look for dependencies on [PyPI](https://pypi.org) by default. Only the name and a version string are required in this case. ```toml [tool.poetry.dependencies] requests = "^2.13.0" ``` If you want to use a [private repository]({{< relref "repositories#using-a-private-repository" >}}), you can add it to your `pyproject.toml` file, like so: ```toml [[tool.poetry.source]] name = "private" url = "http://example.com/simple" ``` If you have multiple repositories configured, you can explicitly tell poetry where to look for a specific package: ```toml [tool.poetry.dependencies] requests = { version = "^2.13.0", source = "private" } ``` You may also specify your project's compatible python versions in this section, instead of or in addition to `project.requires-python`. ```toml [tool.poetry.dependencies] python = "^3.7" ``` {{% note %}} If you specify the compatible python versions in both `tool.poetry.dependencies` and in `project.requires-python`, then Poetry will use the information in `tool.poetry.dependencies` for locking, but the python versions must be a subset of those allowed by `project.requires-python`. For example, the following is invalid and will result in an error, because versions `4.0` and greater are allowed by `tool.poetry.dependencies`, but not by `project.requires-python`. ```toml [project] # ... requires-python = ">=3.8,<4.0" [tool.poetry.dependencies] python = ">=3.8" # not valid! ``` {{% /note %}} You can organize your dependencies in [groups]({{< relref "managing-dependencies#dependency-groups" >}}) to manage them in a more granular way. ```toml [tool.poetry.group.test.dependencies] pytest = "*" [tool.poetry.group.docs.dependencies] mkdocs = "*" ``` See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for a more in-depth look at how to manage dependency groups and [Dependency specification]({{< relref "dependency-specification" >}}) for more information on other keys and specifying version ranges. ### scripts {{% note %}} **Deprecated**: Use [`project.scripts`]({{< relref "#scripts" >}}) instead for `console` and `gui` scripts. Use `[tool.poetry.scripts]` only for scripts of type `file`. {{% /note %}} This section describes the scripts or executables that will be installed when installing the package ```toml [tool.poetry.scripts] my_package_cli = 'my_package.console:run' ``` Here, we will have the `my_package_cli` script installed which will execute the `run` function in the `console` module in the `my_package` package. {{% note %}} When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. {{% /note %}} ```toml [tool.poetry.scripts] my_executable = { reference = "some_binary.exe", type = "file" } ``` This tells Poetry to include the specified file, relative to your project directory, in distribution builds. It will then be copied to the appropriate installation directory for your operating system when your package is installed. * On Windows the file is placed in the `Scripts/` directory. * On *nix system the file is placed in the `bin/` directory. In its table form, the value of each script can contain a `reference` and `type`. The supported types are `console` and `file`. When the value is a string, it is inferred to be a `console` script. ### extras **Deprecated**: Use `project.optional-dependencies` instead. Poetry supports extras to allow expression of: * optional dependencies, which enhance a package, but are not required; and * clusters of optional dependencies. ```toml [tool.poetry] name = "awesome" [tool.poetry.dependencies] # These packages are mandatory and form the core of this package’s distribution. mandatory = "^1.0" # A list of all of the optional dependencies, some of which are included in the # below `extras`. They can be opted into by apps. psycopg2 = { version = "^2.9", optional = true } mysqlclient = { version = "^1.3", optional = true } [tool.poetry.extras] mysql = ["mysqlclient"] pgsql = ["psycopg2"] databases = ["mysqlclient", "psycopg2"] ``` When installing packages with Poetry, you can specify extras by using the `-E|--extras` option: ```bash poetry install --extras "mysql pgsql" poetry install -E mysql -E pgsql ``` Any extras you don't specify will be removed. Note this behavior is different from [optional dependency groups]({{< relref "managing-dependencies#optional-groups" >}}) not selected for installation, e.g., those not specified via `install --with`. You can install all extras with the `--all-extras` option: ```bash poetry install --all-extras ``` {{% note %}} Note that `install --extras` and the variations mentioned above (`--all-extras`, `--extras foo`, etc.) only work on dependencies defined in the current project. If you want to install extras defined by dependencies, you'll have to express that in the dependency itself: ```toml [tool.poetry.dependencies] pandas = {version="^2.2.1", extras=["computation", "performance"]} ``` ```toml [tool.poetry.group.dev.dependencies] fastapi = {version="^0.92.0", extras=["all"]} ``` {{% /note %}} When installing or specifying Poetry-built packages, the extras defined in this section can be activated as described in [PEP 508](https://www.python.org/dev/peps/pep-0508/#extras). For example, when installing the package using `pip`, the dependencies required by the `databases` extra can be installed as shown below. ```bash pip install awesome[databases] ``` {{% note %}} The dependencies specified for each `extra` must already be defined as project dependencies. Dependencies listed in [dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) cannot be specified as extras. {{% /note %}} ### plugins **Deprecated**: Use `project.entry-points` instead. Poetry supports arbitrary plugins, which are exposed as the ecosystem-standard [entry points](https://packaging.python.org/en/latest/specifications/entry-points/) and discoverable using `importlib.metadata`. This is similar to (and compatible with) the entry points feature of `setuptools`. The syntax for registering a plugin is: ```toml [tool.poetry.plugins] # Optional super table [tool.poetry.plugins."A"] B = "C:D" ``` Which are: - `A` - type of the plugin, for example `poetry.plugin` or `flake8.extension` - `B` - name of the plugin - `C` - python module import path - `D` - the entry point of the plugin (a function or class) Example (from [`poetry-plugin-export`](http://github.com/python-poetry/poetry-plugin-export)): ```toml [tool.poetry.plugins."poetry.application.plugin"] export = "poetry_plugin_export.plugins:ExportApplicationPlugin" ``` ### urls **Deprecated**: Use `project.urls` instead. In addition to the basic urls (`homepage`, `repository` and `documentation`), you can specify any custom url in the `urls` section. ```toml [tool.poetry.urls] "Bug Tracker" = "https://github.com/python-poetry/poetry/issues" ``` If you publish your package on PyPI, they will appear in the `Project Links` section. ### requires-poetry A constraint for the Poetry version that is required for this project. If you are using a Poetry version that is not allowed by this constraint, an error will be raised. ```toml [tool.poetry] # ... requires-poetry = ">=2.0" ``` ### requires-plugins In this section, you can specify that certain plugins are required for your project: ```toml [tool.poetry.requires-plugins] my-application-plugin = ">=1.0" my-plugin = ">=1.0,<2.0" ``` See [Project plugins]({{< relref "plugins#project-plugins" >}}) for more information. ### build-constraints In this section, you can specify additional constraints to apply when creating the build environment for a dependency. This is useful if a package does not provide wheels (or shall be built from source for other reasons) and specifies too loose build requirements (without an upper bound) and is not compatible with current versions of one of its build requirements. For example, if your project depends on `some-package`, which only provides an sdist and defines its build requirements as `build-requires = ["setuptools"]`, but is incompatible with `setuptools >= 78`, building the package will probably fail because per default the latest setuptools will be chosen. In this case, you can work around this issue of `some-package` as follows: ```toml [tool.poetry.build-constraints] some-package = { setuptools = "<78" } ``` The syntax for specifying constraints is the same as for specifying dependencies in the `tool.poetry` section. ## Poetry and PEP-517 [PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way to define alternative build systems to build a Python project. Poetry is compliant with PEP-517, by providing a lightweight core library, so if you use Poetry to manage your Python project, you should reference it in the `build-system` section of the `pyproject.toml` file like so: ```toml [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ``` {{% note %}} When using the `new` or `init` command this section will be automatically added. {{% /note %}} {{% note %}} If your `pyproject.toml` file still references `poetry` directly as a build backend, you should update it to reference `poetry-core` instead. {{% /note %}} ================================================ FILE: docs/repositories.md ================================================ --- title: "Repositories" draft: false type: docs layout: "docs" menu: docs: weight: 50 --- # Repositories Poetry supports the use of [PyPI](https://pypi.org) and private repositories for discovery of packages as well as for publishing your projects. By default, Poetry is configured to use the [PyPI](https://pypi.org) repository, for package installation and publishing. So, when you add dependencies to your project, Poetry will assume they are available on PyPI. This represents most cases and will likely be enough for most users. ### Private Repository Example #### Installing from private package sources By default, Poetry discovers and installs packages from [PyPI](https://pypi.org). But, you want to install a dependency to your project for a [simple API repository](#simple-api-repository)? Let's do it. First, [configure](#project-configuration) the [package source](#package-sources) as a [supplemental](#supplemental-package-sources) (or [explicit](#explicit-package-sources)) package source to your project. ```bash poetry source add --priority=supplemental foo https://pypi.example.org/simple/ ``` Then, assuming the repository requires authentication, configure credentials for it. ```bash poetry config http-basic.foo ``` {{% warning %}} If you have completed configuring credentials and are receiving authorization failures, check for the presence of `~/.netrc`, which has been known to conflict with Poetry's configured authentication. {{% /warning %}} {{% warning %}} Depending on your system configuration, credentials might be saved in your command line history. Many shells do not save commands to history when they are prefixed by a space character. For more information, please refer to your shell's documentation. {{% /warning %}} {{% note %}} If you would like to provide the password interactively, you can simply omit `` in your command. And Poetry will prompt you to enter the credential manually. ```bash poetry config http-basic.foo ``` {{% /note %}} Once this is done, you can add dependencies to your project from this source. ```bash poetry add --source foo private-package ``` #### Publishing to a private repository Great, now all that is left is to publish your package. Assuming you'd want to share it privately with your team, you can configure the [Upload API](https://warehouse.pypa.io/api-reference/legacy.html#upload-api) endpoint for your [publishable repository](#publishable-repositories). ```bash poetry config repositories.foo https://pypi.example.org/legacy/ ``` {{% note %}} If you need to use a different credential for your [package source](#package-sources), then it is recommended to use a different name for your publishing repository. ```bash poetry config repositories.foo-pub https://pypi.example.org/legacy/ poetry config http-basic.foo-pub ``` {{% /note %}} {{% note %}} When configuring a repository using environment variables, note that correct suffixes need to be used. ```bash export POETRY_REPOSITORIES_FOO_URL=https://pypi.example.org/legacy/ export POETRY_HTTP_BASIC_FOO_USERNAME= export POETRY_HTTP_BASIC_FOO_PASSWORD= ``` {{% /note %}} Now, all that is left is to build and publish your project using the [`publish`]({{< relref "cli#publish" >}}). ```bash poetry publish --build --repository foo-pub ``` ## Package Sources By default, if you have not configured any primary source, Poetry is configured to use the Python ecosystem's canonical package index [PyPI](https://pypi.org). You can alter this behavior and exclusively look up packages only from the configured package sources by adding at least one primary source. {{% note %}} Except for the implicitly configured source for [PyPI](https://pypi.org) named `PyPI`, package sources are local to a project and must be configured within the project's [`pyproject.toml`]({{< relref "pyproject" >}}) file. This is **not** the same configuration used when publishing a package. {{% /note %}} {{% warning %}} Package sources are a Poetry-specific feature and **not** included in [core metadata](https://packaging.python.org/en/latest/specifications/core-metadata/) produced by the poetry-core build backend. Consequently, when a Poetry project is e.g., installed using Pip (as a normal package or in editable mode), package sources will be ignored and the dependencies in question downloaded from PyPI by default. {{% /warning %}} ### Project Configuration These package sources may be managed using the [`source`]({{< relref "cli#source" >}}) command for your project. ```bash poetry source add foo https://foo.bar/simple/ ``` {{% note %}} If your package source requires [credentials](#configuring-credentials) or [certificates](#certificates), please refer to the relevant sections below. {{% /note %}} This will generate the following configuration snippet in your [`pyproject.toml`]({{< relref "pyproject" >}}) file. ```toml [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" priority = "primary" ``` If `priority` is undefined, the source is considered a primary source, which disables the implicit PyPI source and takes precedence over supplemental sources. Package sources are considered in the following order: 1. [primary sources](#primary-package-sources) or implicit PyPI (if there are no primary sources), 2. [supplemental sources](#supplemental-package-sources). [Explicit sources](#explicit-package-sources) are considered only for packages that explicitly [indicate their source](#package-source-constraint). Within each priority class, package sources are considered in order of appearance in `pyproject.toml`. #### Primary Package Sources All primary package sources are searched for each dependency without a [source constraint](#package-source-constraint). If you configure at least one primary source, the implicit PyPI source is disabled. ```bash poetry source add --priority=primary foo https://foo.bar/simple/ ``` Sources without a priority are considered primary sources, too. ```bash poetry source add foo https://foo.bar/simple/ ``` {{% warning %}} The implicit PyPI source is disabled automatically if at least one primary source is configured. If you want to use PyPI in addition to a primary source, configure it explicitly with a certain priority, e.g. ```bash poetry source add --priority=primary PyPI ``` This way, the priority of PyPI can be set in a fine-granular way. The equivalent specification in `pyproject.toml` is: ```toml [[tool.poetry.source]] name = "pypi" priority = "primary" ``` **Omit the `url` when specifying PyPI explicitly.** Because PyPI is internally configured with Poetry, the PyPI repository cannot be configured with a given URL. Remember, you can always use `poetry check` to ensure the validity of the `pyproject.toml` file. {{% /warning %}} #### Supplemental Package Sources *Introduced in 1.5.0* Package sources configured as supplemental are only searched if no other (higher-priority) source yields a compatible package distribution. This is particularly convenient if the response time of the source is high and relatively few package distributions are to be fetched from this source. You can configure a package source as a supplemental source with `priority = "supplemental"` in your package source configuration. ```bash poetry source add --priority=supplemental foo https://foo.bar/simple/ ``` There can be more than one supplemental package source. {{% warning %}} Take into account that someone could publish a new package to a primary source which matches a package in your supplemental source. They could coincidentally or intentionally replace your dependency with something you did not expect. {{% /warning %}} #### Explicit Package Sources *Introduced in 1.5.0* If package sources are configured as explicit, these sources are only searched when a package configuration [explicitly indicates](#package-source-constraint) that it should be found on this package source. You can configure a package source as an explicit source with `priority = "explicit"` in your package source configuration. ```bash poetry source add --priority=explicit foo https://foo.bar/simple/ ``` There can be more than one explicit package source. {{% note %}} A real-world example where an explicit package source is useful, is for PyTorch GPU packages. ```bash poetry source add --priority=explicit pytorch-gpu-src https://download.pytorch.org/whl/cu118 poetry add --source pytorch-gpu-src torch torchvision torchaudio ``` {{% /note %}} #### Package Source Constraint All package sources (including possibly supplemental sources) will be searched during the package lookup process. These network requests will occur for all primary sources, regardless of if the package is found at one or more sources, and all supplemental sources until the package is found. In order to limit the search for a specific package to a particular package repository, you can specify the source explicitly. ```bash poetry add --source internal-pypi httpx ``` This results in the following configuration in `pyproject.toml`: ```toml [tool.poetry.dependencies] ... httpx = { version = "^0.22", source = "internal-pypi" } [[tool.poetry.source]] name = "internal-pypi" url = ... priority = ... ``` {{% note %}} A repository that is configured to be the only source for retrieving a certain package can itself have any priority. In particular, it does not need to have priority `"explicit"`. If a repository is configured to be the source of a package, it will be the only source that is considered for that package and the repository priority will have no effect on the resolution. {{% /note %}} {{% note %}} Package `source` keys are not inherited by their dependencies. In particular, if `package-A` is configured to be found in `source = internal-pypi`, and `package-A` depends on `package-B` that is also to be found on `internal-pypi`, then `package-B` needs to be configured as such in `pyproject.toml`. The easiest way to achieve this is to add `package-B` with a wildcard constraint: ```bash poetry add --source internal-pypi package-B@* ``` This will ensure that `package-B` is searched only in the `internal-pypi` package source. The version constraints on `package-B` are derived from `package-A` (and other client packages), as usual. If you want to avoid additional main dependencies, you can add `package-B` to a dedicated [dependency group]({{< relref "managing-dependencies#dependency-groups" >}}): ```bash poetry add --group explicit --source internal-pypi package-B@* ``` {{% /note %}} {{% note %}} Package source constraints are strongly suggested for all packages that are expected to be provided only by one specific source to avoid dependency confusion attacks. {{% /note %}} ### Supported Package Sources #### Python Package Index (PyPI) Poetry interacts with [PyPI](https://pypi.org) via its [JSON API](https://warehouse.pypa.io/api-reference/json.html). This is used to retrieve a requested package's versions, metadata, files, etc. {{% note %}} If the package's published metadata is invalid, Poetry will download the available bdist/sdist to inspect it locally to identify the relevant metadata. {{% /note %}} If you want to explicitly select a package from [PyPI](https://pypi.org) you can use the `--source` option with the [`add`]({{< relref "cli#add" >}}) command, like shown below. ```bash poetry add --source pypi httpx@^0.22.0 ``` This will generate the following configuration snippet in your `pyproject.toml` file. ```toml httpx = {version = "^0.22.0", source = "pypi"} ``` {{% warning %}} The implicit `PyPI` source will be disabled and not used for any packages if at least one [primary source](#primary-package-sources) is configured. {{% /warning %}} #### Simple API Repository Poetry can fetch and install package dependencies from public or private custom repositories that implement the simple repository API as described in [PEP 503](https://peps.python.org/pep-0503/). {{% warning %}} When using sources that distribute large wheels without providing file checksum in file URLs, Poetry will download each candidate wheel at least once in order to generate the checksum. This can manifest as long dependency resolution times when adding packages from this source. {{% /warning %}} These package sources may be configured via the following command in your project. ```bash poetry source add testpypi https://test.pypi.org/simple/ ``` {{% note %}} Note the trailing `/simple/`. This is important when configuring [PEP 503](https://peps.python.org/pep-0503/) compliant package sources. {{% /note %}} In addition to [PEP 503](https://peps.python.org/pep-0503/), Poetry can also handle simple API repositories that implement [PEP 658](https://peps.python.org/pep-0658/) (*Introduced in 1.2.0*). This is helpful in reducing dependency resolution time for packages from these sources as Poetry can avoid having to download each candidate distribution, in order to determine associated metadata. {{% note %}} *Why does Poetry insist on downloading all candidate distributions for all platforms when metadata is not available?* The need for this stems from the fact that Poetry's lock file is platform-agnostic. This means, in order to resolve dependencies for a project, Poetry needs metadata for all platform-specific distributions. And when this metadata is not readily available, downloading the distribution and inspecting it locally is the only remaining option. {{% /note %}} #### Single Page Link Source *Introduced in 1.2.0* Some projects choose to release their binary distributions via a single page link source that partially follows the structure of a package page in [PEP 503](https://peps.python.org/pep-0503/). These package sources may be configured via the following command in your project. ```bash poetry source add jax https://storage.googleapis.com/jax-releases/jax_releases.html ``` {{% note %}} All caveats regarding slower resolution times described for simple API repositories do apply here as well. {{% /note %}} ## Publishable Repositories Poetry treats repositories to which you publish packages as user-specific and not project-specific configuration unlike [package sources](#package-sources). Poetry, today, only supports the [Legacy Upload API](https://warehouse.pypa.io/api-reference/legacy.html#upload-api) when publishing your project. These are configured using the [`config`]({{< relref "cli#config" >}}) command, under the `repositories` key. ```bash poetry config repositories.testpypi https://test.pypi.org/legacy/ ``` {{% note %}} [Legacy Upload API](https://warehouse.pypa.io/api-reference/legacy.html#upload-api) URLs are typically different to the same one provided by the repository for the simple API. You'll note that in the example of [Test PyPI](https://test.pypi.org/), both the host (`test.pypi.org`) as well as the path (`/legacy`) are different to its simple API (`https://test.pypi.org/simple`). {{% /note %}} ## Configuring Credentials If you want to store your credentials for a specific repository, you can do so easily: ```bash poetry config http-basic.foo ``` If you do not specify the password, you will be prompted to write it. {{% note %}} To publish to PyPI, you can set your credentials for the repository named `pypi`. Note that it is recommended to use [API tokens](https://pypi.org/help/#apitoken) when uploading packages to PyPI. Once you have created a new token, you can tell Poetry to use it: ```bash poetry config pypi-token.pypi ``` If you have configured **testpypi** as a [Publishable Repository](#publishable-repositories), the token can be set using ```bash poetry config pypi-token.testpypi ``` If you still want to use your username and password, you can do so with the following call to `config`. ```bash poetry config http-basic.pypi ``` {{% /note %}} You can also specify the username and password when using the `publish` command with the `--username` and `--password` options. If a system keyring is available and supported, the password is stored to and retrieved from the keyring. In the above example, the credential will be stored using the name `poetry-repository-pypi`. If access to keyring fails or is unsupported, this will fall back to writing the password to the `auth.toml` file along with the username. Keyring support is enabled using the [keyring library](https://pypi.org/project/keyring/). For more information on supported backends refer to the [library documentation](https://keyring.readthedocs.io/en/latest/?badge=latest). If you do not want to use the keyring, you can tell Poetry to disable it and store the credentials in plaintext config files: ```bash poetry config keyring.enabled false ``` {{% note %}} Poetry will fall back to Pip style use of keyring so that backends like Microsoft's [artifacts-keyring](https://pypi.org/project/artifacts-keyring/) get a chance to retrieve valid credentials. It will need to be properly installed into Poetry's virtualenv, preferably by installing a plugin. {{% /note %}} Alternatively, you can use environment variables to provide the credentials: ```bash export POETRY_PYPI_TOKEN_FOO=my-token export POETRY_HTTP_BASIC_FOO_USERNAME= export POETRY_HTTP_BASIC_FOO_PASSWORD= ``` where `FOO` is the name of the repository in uppercase (e.g. `PYPI`). See [Using environment variables]({{< relref "configuration#using-environment-variables" >}}) for more information on how to configure Poetry with environment variables. If your password starts with a dash (e.g., randomly generated tokens in a CI environment), it will be parsed as a command line option instead of a password. You can prevent this by adding double dashes to prevent any following argument from being parsed as an option. ```bash poetry config -- http-basic.pypi myUsername -myPasswordStartingWithDash ``` {{% note %}} In some cases like that of [Gemfury](https://gemfury.com/help/errors/repo-url-password/) repositories, it might be required to set an empty password. This is supported by Poetry. ```bash poetry config http-basic.foo "" ``` **Note:** Empty usernames are discouraged. However, Poetry will honor them if a password is configured without it. This is unfortunately commonplace practice, while not best practice, for private indices that use tokens. When a password is stored into the system keyring with an empty username, Poetry will use a literal `__poetry_source_empty_username__` as the username to circumvent [keyring#687](https://github.com/jaraco/keyring/pull/687). {{% /note %}} ## Certificates ### Custom certificate authority and mutual TLS authentication Poetry supports repositories that are secured by a custom certificate authority as well as those that require certificate-based client authentication. The following will configure the "foo" repository to validate the repository's certificate using a custom certificate authority and use a client certificate (note that these config variables do not both need to be set): ```bash poetry config certificates.foo.cert /path/to/ca.pem poetry config certificates.foo.client-cert /path/to/client.pem ``` {{% note %}} The value of `certificates..cert` can be set to `false` if certificate verification is required to be skipped. This is useful for cases where a package source with self-signed certificates is used. ```bash poetry config certificates.foo.cert false ``` {{% warning %}} Disabling certificate verification is not recommended as it does not conform to security best practices. {{% /warning %}} {{% /note %}} ## Caches Poetry employs multiple caches for package sources in order to improve user experience and avoid duplicate network requests. The first level cache is a [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header-based cache for almost all HTTP requests. Further, every HTTP backed package source caches metadata associated with a package once it is fetched or generated. Additionally, downloaded files (package distributions) are also cached. ## Debugging Issues If you encounter issues with package sources, one of the simplest steps you might take to debug an issue is rerunning your command with the `--no-cache` flag. ```bash poetry --no-cache add pycowsay ``` If this solves your issue, you can consider clearing your cache using the [`cache`]({{< relref "cli#cache-clear" >}}) command. Alternatively, you could also consider enabling very verbose logging `-vvv` along with the `--no-cache` to see network requests being made in the logs. ================================================ FILE: pyproject.toml ================================================ [project] name = "poetry" version = "2.3.2" description = "Python dependency management and packaging made easy." requires-python = ">=3.10,<4.0" dependencies = [ "poetry-core (==2.3.1)", "build (>=1.2.1,<2.0.0)", "cachecontrol[filecache] (>=0.14.0,<0.15.0)", "cleo (>=2.1.0,<3.0.0)", "dulwich (>=0.25.0,<2)", "fastjsonschema (>=2.18.0,<3.0.0)", "installer (>=0.7.0,<0.8.0)", "keyring (>=25.1.0,<26.0.0)", # packaging uses calver, so version is unclamped "packaging (>=24.2)", # PEP 639 support was added in 24.2 "pkginfo (>=1.12,<2.0)", "platformdirs (>=3.0.0,<5)", "pyproject-hooks (>=1.0.0,<2.0.0)", "requests (>=2.26,<3.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "shellingham (>=1.5,<2.0)", "tomli (>=2.0.1,<3.0.0) ; python_version < '3.11'", "tomlkit (>=0.11.4,<1.0.0)", # trove-classifiers uses calver, so version is unclamped "trove-classifiers (>=2022.5.19)", "virtualenv (>=20.26.6)", "xattr (>=1.0.0,<2.0.0) ; sys_platform == 'darwin'", "findpython (>=0.6.2,<0.8.0)", # pbs-installer uses calver, so version is unclamped "pbs-installer[download,install] (>=2025.6.10)", ] authors = [ { name = "Sébastien Eustace", email = "sebastien@eustace.io" } ] maintainers = [ { name = "Arun Babu Neelicattu", email = "arun.neelicattu@gmail.com" }, { name = "Bjorn Neergaard", email = "bjorn@neersighted.com" }, { name = "Branch Vincent", email = "branchevincent@gmail.com" }, { name = "Randy Döring", email = "radoering.poetry@gmail.com" }, { name = "Steph Samson", email = "hello@stephsamson.com" }, { name = "finswimmer", email = "finswimmer77@gmail.com" }, { name = "Bartosz Sokorski", email = "b.sokorski@gmail.com" }, ] license = "MIT" readme = "README.md" keywords = ["packaging", "dependency", "poetry"] # classifiers are dynamic because we want to create Python classifiers automatically dynamic = [ "classifiers" ] [project.urls] Homepage = "https://python-poetry.org/" Changelog = "https://python-poetry.org/history/" Repository = "https://github.com/python-poetry/poetry" Documentation = "https://python-poetry.org/docs" [project.scripts] poetry = "poetry.console.application:main" [tool.poetry] requires-poetry = ">=2.0" classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] include = [{ path = "tests", format = "sdist" }] [tool.poetry.group.dev.dependencies] pre-commit = ">=2.10" [tool.poetry.group.test.dependencies] coverage = ">=7.2.0" deepdiff = ">=6.3" responses = ">=0.25.8" jaraco-classes = ">=3.3.1" pytest = ">=8.0" pytest-cov = ">=4.0" pytest-mock = ">=3.9" pytest-randomly = ">=3.12" pytest-xdist = { version = ">=3.1", extras = ["psutil"] } [tool.poetry.group.typing.dependencies] mypy = ">=1.8.0" types-requests = ">=2.28.8" # only used in github actions [tool.poetry.group.github-actions] optional = true [tool.poetry.group.github-actions.dependencies] pytest-github-actions-annotate-failures = "^0.1.7" [build-system] requires = ["poetry-core>=2.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] extend-exclude = [ "docs/*", # External to the project's coding standards "tests/fixtures/git/*", "tests/fixtures/project_with_setup*/*", ] fix = true line-length = 88 [tool.ruff.lint] extend-select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions "ERA", # flake8-eradicate/eradicate "I", # isort "N", # pep8-naming "PIE", # flake8-pie "PGH", # pygrep "RUF", # ruff checks "SIM", # flake8-simplify "T20", # flake8-print "TC", # flake8-type-checking "TID", # flake8-tidy-imports "UP", # pyupgrade ] ignore = [ "B904", # use 'raise ... from err' "B905", # use explicit 'strict=' parameter with 'zip()' ] extend-safe-fixes = [ "TC", # move import from and to TYPE_CHECKING blocks ] unfixable = [ "ERA", # do not autoremove commented out code ] [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" [tool.ruff.lint.isort] force-single-line = true lines-between-types = 1 lines-after-imports = 2 known-first-party = ["poetry"] known-third-party = ["poetry.core"] required-imports = ["from __future__ import annotations"] [tool.mypy] files = "src, tests" mypy_path = "src" namespace_packages = true explicit_package_bases = true strict = true enable_error_code = [ "ignore-without-code", "redundant-expr", "truthy-bool", ] exclude = [ "tests/fixtures", "tests/masonry/builders/fixtures", "tests/utils/fixtures", ] # use of importlib-metadata backport makes it impossible to satisfy mypy # without some ignores: but we get different sets of ignores at different # python versions. [[tool.mypy.overrides]] module = [ 'poetry.repositories.installed_repository', 'tests.console.commands.self.test_show_plugins', 'tests.repositories.test_installed_repository', 'tests.helpers', ] warn_unused_ignores = false [[tool.mypy.overrides]] module = [ 'deepdiff.*', 'fastjsonschema.*', 'findpython.*', 'requests_toolbelt.*', 'shellingham.*', 'virtualenv.*', 'xattr.*', ] ignore_missing_imports = true [tool.pytest.ini_options] addopts = [ "-n", "logical", "-ra", "--strict-config", "--strict-markers" ] testpaths = ["tests"] markers = [ "network: mark tests that require internet access", "skip_git_mock: mark tests that should not auto-apply git_mock" ] log_cli_level = "INFO" xfail_strict = true [tool.coverage.report] exclude_also = [ "if TYPE_CHECKING:" ] [tool.repo-review] ignore = [ "PY007", "PP302", "PP309", "GH101", "GH212", "PC111", "PC140", "PC160", "PC170", "PC180", "PC180", "PC901", "MY103", "RTD100", ] ================================================ FILE: src/poetry/__main__.py ================================================ from __future__ import annotations import sys if __name__ == "__main__": from poetry.console.application import main sys.exit(main()) ================================================ FILE: src/poetry/__version__.py ================================================ from __future__ import annotations from importlib import metadata __version__ = metadata.version("poetry") ================================================ FILE: src/poetry/config/__init__.py ================================================ ================================================ FILE: src/poetry/config/config.py ================================================ from __future__ import annotations import dataclasses import json import logging import os import re from copy import deepcopy from json import JSONDecodeError from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from packaging.utils import NormalizedName from packaging.utils import canonicalize_name from poetry.config.dict_config_source import DictConfigSource from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR from poetry.locations import DEFAULT_CACHE_DIR from poetry.locations import data_dir from poetry.toml import TOMLFile if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Mapping from collections.abc import Sequence from poetry.config.config_source import ConfigSource def boolean_validator(val: str) -> bool: return val in {"true", "false", "1", "0"} def boolean_normalizer(val: str) -> bool: return val.lower() in ["true", "1"] def int_normalizer(val: str) -> int: return int(val) def build_config_setting_validator(val: str) -> bool: try: value = build_config_setting_normalizer(val) except JSONDecodeError: return False if not isinstance(value, dict): return False for key, item in value.items(): # keys should be string if not isinstance(key, str): return False # items are allowed to be a string if isinstance(item, str): continue # list items should only contain strings is_valid_list = isinstance(item, list) and all(isinstance(i, str) for i in item) if not is_valid_list: return False return True def build_config_setting_normalizer(val: str) -> Mapping[str, str | Sequence[str]]: value: Mapping[str, str | Sequence[str]] = json.loads(val) return value @dataclasses.dataclass class PackageFilterPolicy: policy: dataclasses.InitVar[str | list[str] | None] packages: list[str] = dataclasses.field(init=False) def __post_init__(self, policy: str | list[str] | None) -> None: if not policy: policy = [] elif isinstance(policy, str): policy = self.normalize(policy) self.packages = policy def allows(self, package_name: str) -> bool: if ":all:" in self.packages: return False return ( not self.packages or ":none:" in self.packages or canonicalize_name(package_name) not in self.packages ) def has_exact_package(self, package_name: str) -> bool: return canonicalize_name(package_name) in self.packages @classmethod def is_reserved(cls, name: str) -> bool: return bool(re.match(r":(all|none):", name)) @classmethod def normalize(cls, policy: str) -> list[str]: if boolean_validator(policy): if boolean_normalizer(policy): return [":all:"] else: return [":none:"] return list( { name.strip() if cls.is_reserved(name) else canonicalize_name(name) for name in policy.strip().split(",") if name } ) @classmethod def validator(cls, policy: str) -> bool: if boolean_validator(policy): return True names = policy.strip().split(",") for name in names: if ( not name or (cls.is_reserved(name) and len(names) == 1) or re.match(r"^[a-zA-Z\d_-]+$", name) ): continue return False return True logger = logging.getLogger(__name__) _default_config: Config | None = None class Config: default_config: ClassVar[dict[str, Any]] = { "cache-dir": str(DEFAULT_CACHE_DIR), "data-dir": str(data_dir()), "virtualenvs": { "create": True, "in-project": None, "path": os.path.join("{cache-dir}", "virtualenvs"), "options": { "always-copy": False, "system-site-packages": False, "no-pip": False, }, "use-poetry-python": False, "prompt": "{project_name}-py{python_version}", }, "requests": { "max-retries": 0, }, "installer": { "re-resolve": False, "parallel": True, "max-workers": None, "no-binary": None, "only-binary": None, "build-config-settings": {}, }, "python": {"installation-dir": os.path.join("{data-dir}", "python")}, "solver": { "lazy-wheel": True, }, "system-git-client": False, "keyring": { "enabled": True, }, } def __init__(self, use_environment: bool = True) -> None: self._config = deepcopy(self.default_config) self._use_environment = use_environment self._config_source: ConfigSource = DictConfigSource() self._auth_config_source: ConfigSource = DictConfigSource() @property def config(self) -> dict[str, Any]: return self._config @property def config_source(self) -> ConfigSource: return self._config_source @property def auth_config_source(self) -> ConfigSource: return self._auth_config_source def set_config_source(self, config_source: ConfigSource) -> Config: self._config_source = config_source return self def set_auth_config_source(self, config_source: ConfigSource) -> Config: self._auth_config_source = config_source return self def merge(self, config: dict[str, Any]) -> None: from poetry.utils.helpers import merge_dicts merge_dicts(self._config, config) def all(self) -> dict[str, Any]: def _all(config: dict[str, Any], parent_key: str = "") -> dict[str, Any]: all_ = {} for key in config: value = self.get(parent_key + key) if isinstance(value, dict): if parent_key != "": current_parent = parent_key + key + "." else: current_parent = key + "." all_[key] = _all(config[key], parent_key=current_parent) continue all_[key] = value return all_ return _all(self.config) def raw(self) -> dict[str, Any]: return self._config @staticmethod def _get_environment_repositories() -> dict[str, dict[str, str]]: repositories = {} pattern = re.compile(r"POETRY_REPOSITORIES_(?P[A-Z_]+)_URL") for env_key in os.environ: match = pattern.match(env_key) if match: repositories[match.group("name").lower().replace("_", "-")] = { "url": os.environ[env_key] } return repositories @staticmethod def _get_environment_build_config_settings() -> Mapping[ NormalizedName, Mapping[str, str | Sequence[str]] ]: build_config_settings = {} pattern = re.compile(r"POETRY_INSTALLER_BUILD_CONFIG_SETTINGS_(?P[^.]+)") for env_key in os.environ: if match := pattern.match(env_key): if not build_config_setting_validator(os.environ[env_key]): logger.debug( "Invalid value set for environment variable %s", env_key ) continue build_config_settings[canonicalize_name(match.group("name"))] = ( build_config_setting_normalizer(os.environ[env_key]) ) return build_config_settings @property def repository_cache_directory(self) -> Path: return Path(self.get("cache-dir")).expanduser() / "cache" / "repositories" @property def artifacts_cache_directory(self) -> Path: return Path(self.get("cache-dir")).expanduser() / "artifacts" @property def virtualenvs_path(self) -> Path: path = self.get("virtualenvs.path") if path is None: path = Path(self.get("cache-dir")) / "virtualenvs" return Path(path).expanduser() @property def python_installation_dir(self) -> Path: path = self.get("python.installation-dir") if path is None: path = Path(self.get("data-dir")) / "python" return Path(path).expanduser() @property def installer_max_workers(self) -> int: # This should be directly handled by ThreadPoolExecutor # however, on some systems the number of CPUs cannot be determined # (it raises a NotImplementedError), so, in this case, we assume # that the system only has one CPU. try: default_max_workers = (os.cpu_count() or 1) + 4 except NotImplementedError: default_max_workers = 5 desired_max_workers = self.get("installer.max-workers") if desired_max_workers is None: return default_max_workers return min(default_max_workers, int(desired_max_workers)) def get(self, setting_name: str, default: Any = None) -> Any: """ Retrieve a setting value. """ keys = setting_name.split(".") build_config_settings: Mapping[ NormalizedName, Mapping[str, str | Sequence[str]] ] = {} # Looking in the environment if the setting # is set via a POETRY_* environment variable if self._use_environment: if setting_name == "repositories": # repositories setting is special for now repositories = self._get_environment_repositories() if repositories: return repositories build_config_settings_key = "installer.build-config-settings" if setting_name == build_config_settings_key or setting_name.startswith( f"{build_config_settings_key}." ): build_config_settings = self._get_environment_build_config_settings() else: env = "POETRY_" + "_".join(k.upper().replace("-", "_") for k in keys) env_value = os.getenv(env) if env_value is not None: return self.process(self._get_normalizer(setting_name)(env_value)) value = self._config # merge installer build config settings from the environment for package_name in build_config_settings: value["installer"]["build-config-settings"][package_name] = ( build_config_settings[package_name] ) for key in keys: if key not in value: return self.process(default) value = value[key] if self._use_environment and isinstance(value, dict): # this is a configuration table, it is likely that we missed env vars # in order to capture them recurse, eg: virtualenvs.options return {k: self.get(f"{setting_name}.{k}") for k in value} return self.process(value) def process(self, value: Any) -> Any: if not isinstance(value, str): return value def resolve_from_config(match: re.Match[str]) -> Any: key = match.group(1) config_value = self.get(key) if config_value: return config_value # The key doesn't exist in the config but might be resolved later, # so we keep it as a format variable. return f"{{{key}}}" return re.sub(r"{(.+?)}", resolve_from_config, value) @staticmethod def _get_normalizer(name: str) -> Callable[[str], Any]: if name in { "virtualenvs.create", "virtualenvs.in-project", "virtualenvs.options.always-copy", "virtualenvs.options.no-pip", "virtualenvs.options.system-site-packages", "virtualenvs.use-poetry-python", "installer.re-resolve", "installer.parallel", "solver.lazy-wheel", "system-git-client", "keyring.enabled", }: return boolean_normalizer if name == "virtualenvs.path": return lambda val: str(Path(val)) if name in { "installer.max-workers", "requests.max-retries", }: return int_normalizer if name in ["installer.no-binary", "installer.only-binary"]: return PackageFilterPolicy.normalize if name.startswith("installer.build-config-settings."): return build_config_setting_normalizer return lambda val: val @classmethod def create(cls, reload: bool = False) -> Config: global _default_config if _default_config is None or reload: _default_config = cls() # Load global config config_file = TOMLFile(CONFIG_DIR / "config.toml") if config_file.exists(): logger.debug("Loading configuration file %s", config_file.path) _default_config.merge(config_file.read()) _default_config.set_config_source(FileConfigSource(config_file)) # Load global auth config auth_config_file = TOMLFile(CONFIG_DIR / "auth.toml") if auth_config_file.exists(): logger.debug("Loading configuration file %s", auth_config_file.path) _default_config.merge(auth_config_file.read()) _default_config.set_auth_config_source(FileConfigSource(auth_config_file)) return _default_config ================================================ FILE: src/poetry/config/config_source.py ================================================ from __future__ import annotations import dataclasses import json from abc import ABC from abc import abstractmethod from typing import TYPE_CHECKING from typing import Any from cleo.io.null_io import NullIO if TYPE_CHECKING: from cleo.io.io import IO UNSET = object() class PropertyNotFoundError(ValueError): pass class ConfigSource(ABC): @abstractmethod def get_property(self, key: str) -> Any: ... @abstractmethod def add_property(self, key: str, value: Any) -> None: ... @abstractmethod def remove_property(self, key: str) -> None: ... @dataclasses.dataclass class ConfigSourceMigration: old_key: str new_key: str | None value_migration: dict[Any, Any] = dataclasses.field(default_factory=dict) def dry_run(self, config_source: ConfigSource, io: IO | None = None) -> bool: io = io or NullIO() try: old_value = config_source.get_property(self.old_key) except PropertyNotFoundError: return False new_value = ( self.value_migration[old_value] if self.value_migration else old_value ) msg = f"{self.old_key} = {json.dumps(old_value)}" if self.new_key is not None and new_value is not UNSET: msg += f" -> {self.new_key} = {json.dumps(new_value)}" elif self.new_key is None: msg += " -> Removed from config" elif self.new_key and new_value is UNSET: msg += f" -> {self.new_key} = Not explicit set" io.write_line(msg) return True def apply(self, config_source: ConfigSource) -> None: try: old_value = config_source.get_property(self.old_key) except PropertyNotFoundError: return new_value = ( self.value_migration[old_value] if self.value_migration else old_value ) config_source.remove_property(self.old_key) if self.new_key is not None and new_value is not UNSET: config_source.add_property(self.new_key, new_value) def drop_empty_config_category( keys: list[str], config: dict[Any, Any] ) -> dict[Any, Any]: config_ = {} for key, value in config.items(): if not keys or key != keys[0]: config_[key] = value continue if keys and key == keys[0]: if isinstance(value, dict): value = drop_empty_config_category(keys[1:], value) if value != {}: config_[key] = value return config_ ================================================ FILE: src/poetry/config/dict_config_source.py ================================================ from __future__ import annotations from typing import Any from poetry.config.config_source import ConfigSource from poetry.config.config_source import PropertyNotFoundError class DictConfigSource(ConfigSource): def __init__(self) -> None: self._config: dict[str, Any] = {} @property def config(self) -> dict[str, Any]: return self._config def get_property(self, key: str) -> Any: keys = key.split(".") config = self._config for i, key in enumerate(keys): if key not in config: raise PropertyNotFoundError(f"Key {'.'.join(keys)} not in config") if i == len(keys) - 1: return config[key] config = config[key] def add_property(self, key: str, value: Any) -> None: keys = key.split(".") config = self._config for i, key in enumerate(keys): if key not in config and i < len(keys) - 1: config[key] = {} if i == len(keys) - 1: config[key] = value break config = config[key] def remove_property(self, key: str) -> None: keys = key.split(".") config = self._config for i, key in enumerate(keys): if key not in config: return if i == len(keys) - 1: del config[key] break config = config[key] ================================================ FILE: src/poetry/config/file_config_source.py ================================================ from __future__ import annotations from contextlib import contextmanager from typing import TYPE_CHECKING from typing import Any from tomlkit import document from tomlkit import table from poetry.config.config_source import ConfigSource from poetry.config.config_source import PropertyNotFoundError from poetry.config.config_source import drop_empty_config_category if TYPE_CHECKING: from collections.abc import Iterator from tomlkit.toml_document import TOMLDocument from poetry.toml.file import TOMLFile class FileConfigSource(ConfigSource): def __init__(self, file: TOMLFile) -> None: self._file = file @property def name(self) -> str: return str(self._file.path) @property def file(self) -> TOMLFile: return self._file def get_property(self, key: str) -> Any: keys = key.split(".") config = self.file.read() if self.file.exists() else {} for i, key in enumerate(keys): if key not in config: raise PropertyNotFoundError(f"Key {'.'.join(keys)} not in config") if i == len(keys) - 1: return config[key] config = config[key] def add_property(self, key: str, value: Any) -> None: with self.secure() as toml: config: dict[str, Any] = toml keys = key.split(".") for i, key in enumerate(keys): if key not in config and i < len(keys) - 1: config[key] = table() if i == len(keys) - 1: config[key] = value break config = config[key] def remove_property(self, key: str) -> None: with self.secure() as toml: config: dict[str, Any] = toml keys = key.split(".") current_config = config for i, key in enumerate(keys): if key not in current_config: return if i == len(keys) - 1: del current_config[key] break current_config = current_config[key] current_config = drop_empty_config_category(keys=keys[:-1], config=config) config.clear() config.update(current_config) @contextmanager def secure(self) -> Iterator[TOMLDocument]: if self.file.exists(): initial_config = self.file.read() config = self.file.read() else: initial_config = document() config = document() new_file = not self.file.exists() yield config try: # Ensuring the file is only readable and writable # by the current user mode = 0o600 if new_file: self.file.path.touch(mode=mode) self.file.write(config) except Exception: self.file.write(initial_config) raise ================================================ FILE: src/poetry/config/source.py ================================================ from __future__ import annotations import dataclasses from typing import TYPE_CHECKING from poetry.repositories.repository_pool import Priority if TYPE_CHECKING: from tomlkit.items import Table @dataclasses.dataclass(order=True, eq=True) class Source: name: str url: str = "" priority: Priority = ( Priority.PRIMARY ) # cheating in annotation: str will be converted to Priority in __post_init__ def __post_init__(self) -> None: if isinstance(self.priority, str): self.priority = Priority[self.priority.upper()] def to_dict(self) -> dict[str, str | bool]: return dataclasses.asdict( self, dict_factory=lambda x: { k: v if not isinstance(v, Priority) else v.name.lower() for (k, v) in x if v }, ) def to_toml_table(self) -> Table: from tomlkit import nl from tomlkit import table source_table: Table = table() for key, value in self.to_dict().items(): source_table.add(key, value) source_table.add(nl()) return source_table ================================================ FILE: src/poetry/console/__init__.py ================================================ ================================================ FILE: src/poetry/console/application.py ================================================ from __future__ import annotations import argparse import logging from contextlib import suppress from importlib import import_module from pathlib import Path from typing import TYPE_CHECKING from typing import cast from cleo._utils import find_similar_names from cleo.application import Application as BaseApplication from cleo.events.console_command_event import ConsoleCommandEvent from cleo.events.console_events import COMMAND from cleo.events.event_dispatcher import EventDispatcher from cleo.exceptions import CleoCommandNotFoundError from cleo.exceptions import CleoError from cleo.formatters.style import Style from cleo.io.inputs.argv_input import ArgvInput from poetry.__version__ import __version__ from poetry.console.command_loader import CommandLoader from poetry.console.commands.command import Command from poetry.console.exceptions import PoetryRuntimeError from poetry.utils.helpers import directory from poetry.utils.helpers import ensure_path if TYPE_CHECKING: from collections.abc import Callable from cleo.events.event import Event from cleo.io.inputs.definition import Definition from cleo.io.inputs.input import Input from cleo.io.io import IO from cleo.io.outputs.output import Output from poetry.console.commands.installer_command import InstallerCommand from poetry.poetry import Poetry def load_command(name: str) -> Callable[[], Command]: def _load() -> Command: words = name.split(" ") module = import_module("poetry.console.commands." + ".".join(words)) command_class = getattr(module, "".join(c.title() for c in words) + "Command") command: Command = command_class() return command return _load COMMANDS = [ "about", "add", "build", "check", "config", "init", "install", "lock", "new", "publish", "remove", "run", "search", "show", "sync", "update", "version", # Cache commands "cache clear", "cache list", # Debug commands "debug info", "debug resolve", "debug tags", # Env commands "env activate", "env info", "env list", "env remove", "env use", # Python commands, "python install", "python list", "python remove", # Self commands "self add", "self install", "self lock", "self remove", "self update", "self show", "self show plugins", "self sync", # Source commands "source add", "source remove", "source show", ] # these are special messages to override the default message when a command is not found # in cases where a previously existing command has been moved to a plugin or outright # removed for various reasons COMMAND_NOT_FOUND_PREFIX_MESSAGE = ( "Looks like you're trying to use a Poetry command that is not available." ) COMMAND_NOT_FOUND_MESSAGES = { "shell": """ Since Poetry (2.0.0), the shell command is not installed by default. You can use, - the new env activate command (recommended); or - the shell plugin to install the shell command Documentation: https://python-poetry.org/docs/managing-environments/#activating-the-environment Note that the env activate command is not a direct replacement for shell command. """ } class Application(BaseApplication): def __init__(self) -> None: super().__init__("poetry", __version__) self._poetry: Poetry | None = None self._io: IO | None = None self._disable_plugins = False self._disable_cache = False self._plugins_loaded = False self._working_directory = Path.cwd() self._project_directory: Path | None = None dispatcher = EventDispatcher() dispatcher.add_listener(COMMAND, self.register_command_loggers) dispatcher.add_listener(COMMAND, self.configure_env) dispatcher.add_listener(COMMAND, self.configure_installer_for_event) self.set_event_dispatcher(dispatcher) command_loader = CommandLoader({name: load_command(name) for name in COMMANDS}) self.set_command_loader(command_loader) @property def _default_definition(self) -> Definition: from cleo.io.inputs.option import Option definition = super()._default_definition definition.add_option( Option("--no-plugins", flag=True, description="Disables plugins.") ) definition.add_option( Option( "--no-cache", flag=True, description="Disables Poetry source caches." ) ) definition.add_option( Option( "--project", "-P", flag=False, description=( "Specify another path as the project root." " All command-line arguments will be resolved relative to the current working directory." ), ) ) definition.add_option( Option( "--directory", "-C", flag=False, description=( "The working directory for the Poetry command (defaults to the" " current working directory). All command-line arguments will be" " resolved relative to the given directory." ), ) ) return definition @property def project_directory(self) -> Path: return self._project_directory or self._working_directory @property def poetry(self) -> Poetry: from poetry.factory import Factory if self._poetry is not None: return self._poetry self._poetry = Factory().create_poetry( cwd=self.project_directory, io=self._io, disable_plugins=self._disable_plugins, disable_cache=self._disable_cache, ) return self._poetry @property def command_loader(self) -> CommandLoader: command_loader = self._command_loader assert isinstance(command_loader, CommandLoader) return command_loader def reset_poetry(self) -> None: self._poetry = None def create_io( self, input: Input | None = None, output: Output | None = None, error_output: Output | None = None, ) -> IO: io = super().create_io(input, output, error_output) # Set our own CLI styles formatter = io.output.formatter formatter.set_style("c1", Style("cyan")) formatter.set_style("c2", Style("default", options=["bold"])) formatter.set_style("info", Style("blue")) formatter.set_style("comment", Style("green")) formatter.set_style("warning", Style("yellow")) formatter.set_style("debug", Style("default", options=["dark"])) formatter.set_style("success", Style("green")) # Dark variants formatter.set_style("c1_dark", Style("cyan", options=["dark"])) formatter.set_style("c2_dark", Style("default", options=["bold", "dark"])) formatter.set_style("success_dark", Style("green", options=["dark"])) io.output.set_formatter(formatter) io.error_output.set_formatter(formatter) self._io = io return io def _run(self, io: IO) -> int: # we do this here and not inside the _configure_io implementation in order # to ensure the users are not exposed to a stack trace for providing invalid values to # the options --directory or --project, configuring the options here allow cleo to trap and # display the error cleanly unless the user uses verbose or debug self._configure_global_options(io) with directory(self._working_directory): self._load_plugins(io) exit_code: int = 1 try: exit_code = super()._run(io) except PoetryRuntimeError as e: io.write_error_line("") e.write(io) io.write_error_line("") except CleoCommandNotFoundError as e: command = self._get_command_name(io) if command is not None and ( message := COMMAND_NOT_FOUND_MESSAGES.get(command) ): io.write_error_line("") io.write_error_line(COMMAND_NOT_FOUND_PREFIX_MESSAGE) io.write_error_line(message) return 1 if command is not None and command in self.get_namespaces(): sub_commands = [] for key in self._commands: if key.startswith(f"{command} "): sub_commands.append(key) io.write_error_line( f"The requested command does not exist in the {command} namespace." ) suggested_names = find_similar_names(command, sub_commands) self._error_write_command_suggestions( io, suggested_names, f"#{command}" ) return 1 if command is not None: suggested_names = find_similar_names( command, list(self._commands.keys()) ) io.write_error_line( f"The requested command {command} does not exist." ) self._error_write_command_suggestions(io, suggested_names) return 1 raise e return exit_code def _error_write_command_suggestions( self, io: IO, suggested_names: list[str], doc_tag: str | None = None ) -> None: if suggested_names: suggestion_lines = [ f"{name.replace(' ', ' ', 1)}: {self._commands[name].description}" for name in suggested_names ] suggestions = "\n ".join(["", *sorted(suggestion_lines)]) io.write_error_line( f"\nDid you mean one of these perhaps?{suggestions}" ) io.write_error_line( "\nDocumentation: " f"https://python-poetry.org/docs/cli/{doc_tag or ''}" ) def _configure_global_options(self, io: IO) -> None: """ Configures global options for the application by setting up the relevant directories, disabling plugins or cache, and managing the working and project directories. This method ensures that all directories are valid paths and handles the resolution of the project directory relative to the working directory if necessary. :param io: The IO instance whose input and options are being read. :return: Nothing. """ self._disable_plugins = io.input.option("no-plugins") self._disable_cache = io.input.option("no-cache") # we use ensure_path for the directories to make sure these are valid paths # this will raise an exception if the path is invalid self._working_directory = ensure_path( io.input.option("directory") or Path.cwd(), is_directory=True ) self._project_directory = io.input.option("project") if self._project_directory is not None: self._project_directory = Path(self._project_directory) self._project_directory = ensure_path( self._project_directory if self._project_directory.is_absolute() else self._working_directory.joinpath(self._project_directory).resolve( strict=False ), is_directory=True, ) def _sort_global_options(self, io: IO) -> None: """ Sorts global options of the provided IO instance according to the definition of the available options, reordering and parsing arguments to ensure consistency in input handling. The function interprets the options and their corresponding values using an argument parser, constructs a sorted list of tokens, and recreates the input with the rearranged sequence while maintaining compatibility with the initially provided input stream. If using in conjunction with `_configure_run_command`, it is recommended that it be called first in order to correctly handling cases like `poetry run -V python -V`. :param io: The IO instance whose input and options are being processed and reordered. :return: Nothing. """ original_input = cast("ArgvInput", io.input) tokens: list[str] = original_input._tokens parser = argparse.ArgumentParser(add_help=False) for option in self.definition.options: parser.add_argument( f"--{option.name}", *([f"-{option.shortcut}"] if option.shortcut else []), action="store_true" if option.is_flag() else "store", ) args, remaining_args = parser.parse_known_args(tokens) tokens = [] for option in self.definition.options: key = option.name.replace("-", "_") value = getattr(args, key, None) if value is not None: if value: # is truthy tokens.append(f"--{option.name}") if option.accepts_value(): tokens.append(str(value)) sorted_input = ArgvInput([self._name or "", *tokens, *remaining_args]) # this is required to ensure stdin is transferred sorted_input.set_stream(original_input.stream) # this is required as cleo internally checks for `io.input._interactive` # when configuring io, and cleo's test applications overrides this attribute # explicitly causing test setups to fail sorted_input.interactive(io.input.is_interactive()) with suppress(CleoError): sorted_input.bind(self.definition) io.set_input(sorted_input) def _configure_run_command(self, io: IO) -> None: """ Configures the input for the "run" command to properly handle cases where the user executes commands such as "poetry run -- ". This involves reorganizing input tokens to ensure correct parsing and execution of the run command. """ with suppress(CleoError): io.input.bind(self.definition) command_name = io.input.first_argument if command_name == "run": original_input = cast("ArgvInput", io.input) tokens: list[str] = original_input._tokens if "--" in tokens: # this means the user has done the right thing and used "poetry run -- echo hello" # in this case there is not much we need to do, we can skip the rest return # find the correct command index, in some cases this might not be first occurrence # eg: poetry -C run run echo command_index = tokens.index(command_name) while command_index < (len(tokens) - 1): try: # try parsing the tokens so far _ = ArgvInput( [self._name or "", *tokens[: command_index + 1]], definition=self.definition, ) break except CleoError: # parsing failed, try finding the next "run" token try: command_index += ( tokens[command_index + 1 :].index(command_name) + 1 ) except ValueError: command_index = len(tokens) else: # looks like we reached the end of the road, let cleo deal with it return # fetch tokens after the "run" command tokens_without_command = tokens[command_index + 1 :] # we create a new input for parsing the subcommand pretending # it is poetry command without_command = ArgvInput( [self._name or "", *tokens_without_command], None ) with suppress(CleoError): # we want to bind the definition here so that cleo knows what should be # parsed, and how without_command.bind(self.definition) # the first argument here is the subcommand subcommand = without_command.first_argument subcommand_index = ( (tokens_without_command.index(subcommand) if subcommand else 0) + command_index + 1 ) # recreate the original input reordering in the following order # - all tokens before "run" command # - all tokens after "run" command but before the subcommand # - the "run" command token # - the "--" token to normalise the form # - all remaining tokens starting with the subcommand run_input = ArgvInput( [ self._name or "", *tokens[:command_index], *tokens[command_index + 1 : subcommand_index], command_name, "--", *tokens[subcommand_index:], ] ) run_input.set_stream(original_input.stream) with suppress(CleoError): run_input.bind(self.definition) # reset the input to our constructed form io.set_input(run_input) def _configure_io(self, io: IO) -> None: self._configure_run_command(io) self._sort_global_options(io) super()._configure_io(io) def register_command_loggers( self, event: Event, event_name: str, _: EventDispatcher ) -> None: from poetry.console.logging.filters import POETRY_FILTER from poetry.console.logging.io_formatter import IOFormatter from poetry.console.logging.io_handler import IOHandler assert isinstance(event, ConsoleCommandEvent) command = event.command if not isinstance(command, Command): return io = event.io loggers = [ "poetry.packages.locker", "poetry.packages.package", "poetry.utils.password_manager", ] loggers += command.loggers handler = IOHandler(io) handler.setFormatter(IOFormatter()) level = logging.WARNING if io.is_debug(): level = logging.DEBUG elif io.is_very_verbose() or io.is_verbose(): level = logging.INFO logging.basicConfig(level=level, handlers=[handler]) # only log third-party packages when very verbose if not io.is_very_verbose(): handler.addFilter(POETRY_FILTER) for name in loggers: logger = logging.getLogger(name) _level = level # The builders loggers are special and we can actually # start at the INFO level. if ( logger.name.startswith("poetry.core.masonry.builders") and _level > logging.INFO ): _level = logging.INFO logger.setLevel(_level) def configure_env(self, event: Event, event_name: str, _: EventDispatcher) -> None: from poetry.console.commands.env_command import EnvCommand from poetry.console.commands.self.self_command import SelfCommand assert isinstance(event, ConsoleCommandEvent) command = event.command if not isinstance(command, EnvCommand) or isinstance(command, SelfCommand): return if command._env is not None: return from poetry.utils.env import EnvManager io = event.io poetry = command.poetry env_manager = EnvManager(poetry, io=io) env = env_manager.create_venv() if env.is_venv() and io.is_verbose(): io.write_error_line(f"Using virtualenv: {env.path}") command.set_env(env) @classmethod def configure_installer_for_event( cls, event: Event, event_name: str, _: EventDispatcher ) -> None: from poetry.console.commands.installer_command import InstallerCommand assert isinstance(event, ConsoleCommandEvent) command = event.command if not isinstance(command, InstallerCommand): return # If the command already has an installer # we skip this step if command._installer is not None: return cls.configure_installer_for_command(command, event.io) @staticmethod def configure_installer_for_command(command: InstallerCommand, io: IO) -> None: from poetry.installation.installer import Installer poetry = command.poetry installer = Installer( io, command.env, poetry.package, poetry.locker, poetry.pool, poetry.config, disable_cache=poetry.disable_cache, build_constraints=poetry.build_constraints, ) command.set_installer(installer) def _load_plugins(self, io: IO) -> None: if self._plugins_loaded: return self._disable_plugins = io.input.has_parameter_option("--no-plugins") if not self._disable_plugins: from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin_manager import PluginManager PluginManager.add_project_plugin_path(self.project_directory) manager = PluginManager(ApplicationPlugin.group) manager.load_plugins() manager.activate(self) self._plugins_loaded = True def main() -> int: exit_code: int = Application().run() return exit_code if __name__ == "__main__": main() ================================================ FILE: src/poetry/console/command_loader.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from cleo.exceptions import CleoLogicError from cleo.loaders.factory_command_loader import FactoryCommandLoader if TYPE_CHECKING: from collections.abc import Callable from cleo.commands.command import Command class CommandLoader(FactoryCommandLoader): def register_factory( self, command_name: str, factory: Callable[[], Command] ) -> None: if command_name in self._factories: raise CleoLogicError(f'The command "{command_name}" already exists.') self._factories[command_name] = factory ================================================ FILE: src/poetry/console/commands/__init__.py ================================================ ================================================ FILE: src/poetry/console/commands/about.py ================================================ from __future__ import annotations from poetry.console.commands.command import Command class AboutCommand(Command): name = "about" description = "Shows information about Poetry." def handle(self) -> int: from importlib import metadata self.line( f"""\ Poetry - Package Management for Python Version: {metadata.version("poetry")} Poetry-Core Version: {metadata.version("poetry-core")} Poetry is a dependency manager tracking local dependencies of your projects\ and libraries. See https://github.com/python-poetry/poetry for more information.\ """ ) return 0 ================================================ FILE: src/poetry/console/commands/add.py ================================================ from __future__ import annotations import contextlib from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from typing import Literal from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from tomlkit.toml_document import TOMLDocument from poetry.console.commands.init import InitCommand from poetry.console.commands.installer_command import InstallerCommand if TYPE_CHECKING: from collections.abc import Collection from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from packaging.utils import NormalizedName class AddCommand(InstallerCommand, InitCommand): name = "add" description = "Adds a new dependency to pyproject.toml and installs it." arguments: ClassVar[list[Argument]] = [ argument("name", "The packages to add.", multiple=True) ] options: ClassVar[list[Option]] = [ option( "group", "-G", "The group to add the dependency to.", flag=False, default=MAIN_GROUP, ), option( "dev", "D", "Add as a development dependency. (shortcut for '-G dev')", ), option("editable", "e", "Add vcs/path dependencies as editable."), option( "extras", "E", "Extras to activate for the dependency.", flag=False, multiple=True, ), option( "optional", None, "Add as an optional dependency to an extra.", flag=False, ), option( "python", None, "Python version for which the dependency must be installed.", flag=False, ), option( "platform", None, "Platforms for which the dependency must be installed.", flag=False, ), option( "markers", None, "Environment markers which describe when the dependency should be installed.", flag=False, ), option( "source", None, "Name of the source to use to install the package.", flag=False, ), option("allow-prereleases", None, "Accept prereleases."), option( "dry-run", None, "Output the operations but do not execute anything (implicitly enables" " --verbose).", ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] examples = """\ If you do not specify a version constraint, poetry will choose a suitable one based on\ the available package versions. You can specify a package in the following forms: - A single name (requests) - A name and a constraint (requests@^2.23.0) - A git url (git+https://github.com/python-poetry/poetry.git) - A git url with a revision\ (git+https://github.com/python-poetry/poetry.git#develop) - A subdirectory of a git repository\ (git+https://github.com/python-poetry/poetry.git#subdirectory=tests/fixtures/sample_project) - A git SSH url (git+ssh://git@github.com/python-poetry/poetry.git) - A git SSH url with a revision\ (git+ssh://git@github.com/python-poetry/poetry.git#develop) - A file path (../my-package/my-package.whl) - A directory (../my-package/) - A url (https://example.com/packages/my-package-0.1.0.tar.gz) """ help = f"""\ The add command adds required packages to your pyproject.toml and installs\ them. {examples} """ loggers: ClassVar[list[str]] = [ "poetry.repositories.pypi_repository", "poetry.inspection.info", ] def handle(self) -> int: from poetry.core.constraints.version import parse_constraint from tomlkit import array from tomlkit import inline_table from tomlkit import nl from tomlkit import table from poetry.factory import Factory packages = self.argument("name") if self.option("dev"): group = "dev" else: group = self.option("group", self.default_group or MAIN_GROUP) if self.option("extras") and len(packages) > 1: raise ValueError( "You can only specify one package when using the --extras option" ) optional = self.option("optional") if optional and group != MAIN_GROUP: raise ValueError("You can only add optional dependencies to the main group") # tomlkit types are awkward to work with, treat content as a mostly untyped # dictionary. content: dict[str, Any] = self.poetry.file.read() project_content = content.get("project", table()) poetry_content = content.get("tool", {}).get("poetry", table()) groups_content = content.get("dependency-groups", {}) project_name = ( canonicalize_name(name) if (name := project_content.get("name", poetry_content.get("name"))) else None ) use_project_section = False use_groups_section = False project_dependency_names: list[NormalizedName | Literal[""]] = [] # Run-Time Deps incl. extras if group == MAIN_GROUP: if ( "dependencies" in project_content or "optional-dependencies" in project_content ): use_project_section = True if optional: project_section = project_content.get( "optional-dependencies", {} ).get(optional, array()) else: project_section = project_content.get("dependencies", array()) project_dependency_names = [ Dependency.create_from_pep_508(dep).name for dep in project_section ] else: project_section = array() poetry_section = poetry_content.get("dependencies", table()) # Dependency Groups else: if groups_content or "group" not in poetry_content: use_groups_section = True if not groups_content: groups_content = table(is_super_table=True) if group not in groups_content: groups_content[group] = array("[\n]") project_dependency_names = [ Dependency.create_from_pep_508(dep).name if isinstance(dep, str) # We have to add an entry for "include-group" items because # later the index of the dependency is used to replace it. # We just choose a name that cannot be a package's name. else "" for dep in groups_content[group] ] poetry_section = ( poetry_content.get("group", {}) .get(group, {}) .get("dependencies", table()) ) project_section = [] existing_packages = self.get_existing_packages_from_input( packages, poetry_section, project_dependency_names ) if existing_packages: self.notify_about_existing_packages(existing_packages) packages = [name for name in packages if name not in existing_packages] if not packages: self.line("Nothing to add.") return 0 if optional and not use_project_section: self.line_error( "Optional dependencies will not be added to extras" " in legacy mode. Consider converting your project to use the [project]" " section." ) requirements = self._determine_requirements( packages, allow_prereleases=self.option("allow-prereleases") or None, source=self.option("source"), ) for _constraint in requirements: version = _constraint.get("version") if version is not None: # Validate version constraint assert isinstance(version, str) parse_constraint(version) constraint: dict[str, Any] = inline_table() for key, value in _constraint.items(): if key == "name": continue constraint[key] = value if optional: constraint["optional"] = True if self.option("allow-prereleases"): constraint["allow-prereleases"] = True if self.option("extras"): extras = [] for extra in self.option("extras"): extras += extra.split() constraint["extras"] = extras if self.option("editable"): if "git" in _constraint or "path" in _constraint: constraint["develop"] = True else: self.line_error( "\n" "Failed to add packages. " "Only vcs/path dependencies support editable installs. " f"{_constraint['name']} is neither." ) self.line_error("\nNo changes were applied.") return 1 if python := self.option("python"): constraint["python"] = python if platform := self.option("platform"): constraint["platform"] = platform if markers := self.option("markers"): constraint["markers"] = markers if source := self.option("source"): constraint["source"] = source if len(constraint) == 1 and "version" in constraint: constraint = constraint["version"] constraint_name = _constraint["name"] assert isinstance(constraint_name, str) canonical_constraint_name = canonicalize_name(constraint_name) if canonical_constraint_name == project_name: self.line_error( f"Cannot add dependency on {constraint_name} to" " project with the same name." ) self.line_error("\nNo changes were applied.") return 1 with contextlib.suppress(ValueError): self.poetry.package.dependency_group(group).remove_dependency( constraint_name ) dependency = Factory.create_dependency( constraint_name, constraint, groups=[group], root_dir=self.poetry.file.path.parent, ) self.poetry.package.add_dependency(dependency) if use_project_section or use_groups_section: pep_section = ( project_section if use_project_section else groups_content[group] ) try: index = project_dependency_names.index(canonical_constraint_name) except ValueError: pep_section.append(dependency.to_pep_508()) else: pep_section[index] = dependency.to_pep_508() # create a second constraint for tool.poetry.dependencies with keys # that cannot be stored in the project section poetry_constraint: dict[str, Any] = inline_table() if not isinstance(constraint, str): for key in ["allow-prereleases", "develop", "source"]: if value := constraint.get(key): poetry_constraint[key] = value if poetry_constraint: # add marker related keys to avoid ambiguity for key in ["python", "platform"]: if value := constraint.get(key): poetry_constraint[key] = value else: poetry_constraint = constraint if poetry_constraint: for key in poetry_section: if canonicalize_name(key) == canonical_constraint_name: poetry_section[key] = poetry_constraint break else: poetry_section[constraint_name] = poetry_constraint if optional: extra_name = canonicalize_name(optional) # _in_extras must be set after converting the dependency to PEP 508 # and adding it to the project section to avoid a redundant extra marker dependency._in_extras = [extra_name] self._add_dependency_to_extras(dependency, extra_name) # Refresh the locker if project_section: assert group == MAIN_GROUP if optional: if "optional-dependencies" not in project_content: project_content["optional-dependencies"] = table() if optional not in project_content["optional-dependencies"]: project_content["optional-dependencies"][optional] = project_section elif "dependencies" not in project_content: project_content["dependencies"] = project_section if poetry_section: if "tool" not in content: content["tool"] = table() if "poetry" not in content["tool"]: content["tool"]["poetry"] = poetry_content if group == MAIN_GROUP: if "dependencies" not in poetry_content: poetry_content["dependencies"] = poetry_section else: if "group" not in poetry_content: poetry_content["group"] = table(is_super_table=True) groups = poetry_content["group"] if group not in groups: groups[group] = table() groups.add(nl()) if "dependencies" not in groups[group]: groups[group]["dependencies"] = poetry_section if groups_content and group != MAIN_GROUP: if "dependency-groups" not in content: content["dependency-groups"] = table() content["dependency-groups"][group] = groups_content[group] self.poetry.locker.set_pyproject_data(content) self.installer.set_locker(self.poetry.locker) # Cosmetic new line self.line("") self.installer.set_package(self.poetry.package) self.installer.dry_run(self.option("dry-run")) self.installer.verbose(self.io.is_verbose()) self.installer.update(True) self.installer.execute_operations(not self.option("lock")) self.installer.whitelist([r["name"] for r in requirements]) status = self.installer.run() if status == 0 and not self.option("dry-run"): assert isinstance(content, TOMLDocument) self.poetry.file.write(content) return status def get_existing_packages_from_input( self, packages: list[str], section: dict[str, Any], project_dependencies: Collection[NormalizedName | Literal[""]], ) -> list[str]: existing_packages = [] for name in packages: normalized_name = canonicalize_name(name) if normalized_name in project_dependencies: existing_packages.append(name) continue for key in section: if normalized_name == canonicalize_name(key): existing_packages.append(name) return existing_packages @property def _hint_update_packages(self) -> str: return ( "\nIf you want to update it to the latest compatible version, you can use" " `poetry update package`.\nIf you prefer to upgrade it to the latest" " available version, you can use `poetry add package@latest`.\n" ) def notify_about_existing_packages(self, existing_packages: list[str]) -> None: self.line( "The following packages are already present in the pyproject.toml and will" " be skipped:\n" ) for name in existing_packages: self.line(f" - {name}") self.line(self._hint_update_packages) def _add_dependency_to_extras( self, dependency: Dependency, extra_name: NormalizedName ) -> None: extras = dict(self.poetry.package.extras) extra_deps = [] replaced = False for dep in extras.get(extra_name, ()): if dep.name == dependency.name: extra_deps.append(dependency) replaced = True else: extra_deps.append(dep) if not replaced: extra_deps.append(dependency) extras[extra_name] = extra_deps self.poetry.package.extras = extras ================================================ FILE: src/poetry/console/commands/build.py ================================================ from __future__ import annotations import dataclasses from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from typing import Literal from cleo.helpers import option from poetry.core.constraints.version import Version from poetry.console.commands.env_command import EnvCommand from poetry.masonry.builders import BUILD_FORMATS from poetry.utils.helpers import remove_directory from poetry.utils.isolated_build import isolated_builder if TYPE_CHECKING: from collections.abc import Callable from cleo.io.inputs.option import Option from cleo.io.io import IO from poetry.poetry import Poetry from poetry.utils.env import Env DistributionType = Literal["sdist", "wheel"] @dataclasses.dataclass(frozen=True) class BuildOptions: clean: bool formats: list[DistributionType] output: str config_settings: dict[str, Any] = dataclasses.field(default_factory=dict) def __post_init__(self) -> None: for fmt in self.formats: if fmt not in BUILD_FORMATS: raise ValueError(f"Invalid format: {fmt}") class BuildHandler: def __init__(self, poetry: Poetry, env: Env, io: IO) -> None: self.poetry = poetry self.env = env self.io = io def _build( self, fmt: DistributionType, executable: Path, target_dir: Path, config_settings: dict[str, Any], ) -> None: builder = BUILD_FORMATS[fmt] builder( self.poetry, executable=executable, config_settings=config_settings, ).build(target_dir) def _isolated_build( self, fmt: DistributionType, executable: Path, target_dir: Path, config_settings: dict[str, Any], ) -> None: with isolated_builder( source=self.poetry.file.path.parent, distribution=fmt, python_executable=executable, ) as builder: builder.build(fmt, target_dir, config_settings=config_settings) def _requires_isolated_build(self) -> bool: """ Determines if an isolated build is required. An isolated build is required if: - The package has a build script. - There are multiple build system dependencies. - The build dependency is not `poetry-core`. - The installed `poetry-core` version does not satisfy the build dependency constraints. - The build dependency has a source type (e.g. is a VcsDependency). :returns: True if an isolated build is required, False otherwise. """ if not self._has_build_backend_defined(): self.io.write_error_line( "WARNING: No build backend defined. Please define one in the pyproject.toml.\n" "Falling back to using the built-in `poetry-core` version.\n" "In a future release Poetry will fallback to `setuptools` as defined by PEP 517.\n" "More details can be found at https://python-poetry.org/docs/libraries/#packaging" ) return False if ( self.poetry.package.build_script or len(self.poetry.build_system_dependencies) != 1 ): return True build_dependency = self.poetry.build_system_dependencies[0] if build_dependency.name != "poetry-core": return True poetry_core_version = Version.parse(metadata.version("poetry-core")) return bool( not build_dependency.constraint.allows(poetry_core_version) or build_dependency.source_type ) def _get_builder(self) -> Callable[..., None]: if self._requires_isolated_build(): return self._isolated_build return self._build def _has_build_backend_defined(self) -> bool: return "build-backend" in self.poetry.pyproject.data.get("build-system", {}) def build(self, options: BuildOptions) -> int: if not self.poetry.is_package_mode: self.io.write_error_line( "Building a package is not possible in non-package mode." ) return 1 dist_dir = Path(options.output) package = self.poetry.package self.io.write_line( f"Building {package.pretty_name} ({package.version})" ) if not dist_dir.is_absolute(): dist_dir = self.poetry.pyproject_path.parent / dist_dir if options.clean: remove_directory(path=dist_dir, force=True) build = self._get_builder() for fmt in options.formats: self.io.write_line(f"Building {fmt}") build( fmt, executable=self.env.python, target_dir=dist_dir, config_settings=options.config_settings, ) return 0 class BuildCommand(EnvCommand): name = "build" description = "Builds a package, as a tarball and a wheel by default." options: ClassVar[list[Option]] = [ option("format", "f", "Limit the format to either sdist or wheel.", flag=False), option( "clean", description="Clean output directory before building.", flag=True, ), option( "local-version", "l", "Add or replace a local version label to the build. (Deprecated)", flag=False, ), option( "output", "o", "Set output directory for build artifacts. Default is `dist`.", default="dist", flag=False, ), option( "config-settings", "c", description="Provide config settings that should be passed to backend in = format.", flag=False, multiple=True, ), ] loggers: ClassVar[list[str]] = [ "poetry.core.masonry.builders.builder", "poetry.core.masonry.builders.sdist", "poetry.core.masonry.builders.wheel", ] @staticmethod def _prepare_config_settings( local_version: str | None, config_settings: list[str] | None, io: IO ) -> dict[str, str]: config_settings = config_settings or [] result = {} if local_version: io.write_error_line( f"`--local-version` is deprecated." f" Use `--config-settings local-version={local_version}`" f" instead." ) result["local-version"] = local_version for config_setting in config_settings: if "=" not in config_setting: raise ValueError( f"Invalid config setting format: {config_setting}. " "Config settings must be in the format 'key=value'" ) key, _, value = config_setting.partition("=") result[key] = value return result @staticmethod def _prepare_formats(fmt: str | None) -> list[str]: fmt = fmt or "all" return ["sdist", "wheel"] if fmt == "all" else [fmt] def handle(self) -> int: build_handler = BuildHandler( poetry=self.poetry, env=self.env, io=self.io, ) build_options = BuildOptions( clean=self.option("clean"), formats=self._prepare_formats(self.option("format")), # type: ignore[arg-type] output=self.option("output"), config_settings=self._prepare_config_settings( local_version=self.option("local-version"), config_settings=self.option("config-settings"), io=self.io, ), ) return build_handler.build(options=build_options) ================================================ FILE: src/poetry/console/commands/cache/__init__.py ================================================ ================================================ FILE: src/poetry/console/commands/cache/clear.py ================================================ from __future__ import annotations import os from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name from poetry.config.config import Config from poetry.console.commands.command import Command from poetry.utils.cache import FileCache if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class CacheClearCommand(Command): name = "cache clear" description = "Clear Poetry's caches." arguments: ClassVar[list[Argument]] = [ argument("cache", description="The name of the cache to clear.", optional=True) ] options: ClassVar[list[Option]] = [ option("all", description="Clear all entries in the cache.") ] def handle(self) -> int: cache = self.argument("cache") if cache: parts = cache.split(":") root = parts[0] else: parts = [] root = "" config = Config.create() cache_dir = config.repository_cache_directory / root try: cache_dir.relative_to(config.repository_cache_directory) except ValueError: raise ValueError(f"{root} is not a valid repository cache") cache = FileCache(cache_dir) if len(parts) < 2: if not self.option("all"): raise RuntimeError( "Add the --all option if you want to clear all cache entries" ) if not cache_dir.exists(): self.line( f"No cache entries for {root}" if root else "No cache entries" ) return 0 # Calculate number of entries entries_count = sum( len(files) for _path, _dirs, files in os.walk(str(cache_dir)) ) delete = self.confirm(f"Delete {entries_count} entries?", True) if not delete: return 0 cache.flush() elif len(parts) == 2: raise RuntimeError( "Only specifying the package name is not yet supported. " "Add a specific version to clear" ) elif len(parts) == 3: package = canonicalize_name(parts[1]) version = parts[2] if not cache.has(f"{package}:{version}"): self.line(f"No cache entries for {package}:{version}") return 0 delete = self.confirm(f"Delete cache entry {package}:{version}", True) if not delete: return 0 cache.forget(f"{package}:{version}") else: raise ValueError("Invalid cache key") return 0 ================================================ FILE: src/poetry/console/commands/cache/list.py ================================================ from __future__ import annotations from poetry.config.config import Config from poetry.console.commands.command import Command class CacheListCommand(Command): name = "cache list" description = "List Poetry's caches." def handle(self) -> int: config = Config.create() if config.repository_cache_directory.exists(): caches = sorted(config.repository_cache_directory.iterdir()) if caches: for cache in caches: self.line(f"{cache.name}") return 0 self.line_error("No caches found") return 0 ================================================ FILE: src/poetry/console/commands/check.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from cleo.helpers import option from poetry.console.commands.command import Command if TYPE_CHECKING: from pathlib import Path from cleo.io.inputs.option import Option class CheckCommand(Command): name = "check" description = ( "Validates the content of the pyproject.toml file and its" " consistency with the poetry.lock file." ) options: ClassVar[list[Option]] = [ option( "lock", None, "Checks that poetry.lock exists for the current" " version of pyproject.toml.", ), option( "strict", None, "Fail if check reports warnings.", ), ] def _validate_classifiers( self, project_classifiers: set[str] ) -> tuple[list[str], list[str]]: """Identify unrecognized and deprecated trove classifiers. A fully-qualified classifier is a string delimited by `` :: `` separators. To make the error message more readable we need to have visual clues to materialize the start and end of a classifier string. That way the user can easily copy and paste it from the messages while reducing mistakes because of extra spaces. We use ``!r`` (``repr()``) for classifiers and list of classifiers for consistency. That way all strings will be rendered with the same kind of quotes (i.e. simple tick: ``'``). """ from trove_classifiers import classifiers from trove_classifiers import deprecated_classifiers errors = [] warnings = [] unrecognized = sorted( project_classifiers - set(classifiers) - set(deprecated_classifiers) ) # Allow "Private ::" classifiers as recommended on PyPI and the packaging guide # to allow users to avoid accidentally publishing private packages to PyPI. # https://pypi.org/classifiers/ unrecognized = [u for u in unrecognized if not u.startswith("Private ::")] if unrecognized: errors.append(f"Unrecognized classifiers: {unrecognized!r}.") deprecated = sorted( project_classifiers.intersection(set(deprecated_classifiers)) ) if deprecated: for old_classifier in deprecated: new_classifiers = deprecated_classifiers[old_classifier] if new_classifiers: message = ( f"Deprecated classifier {old_classifier!r}. " f"Must be replaced by {new_classifiers!r}." ) else: message = ( f"Deprecated classifier {old_classifier!r}. Must be removed." ) warnings.append(message) return errors, warnings def _validate_readme(self, readme: str | list[str], poetry_file: Path) -> list[str]: """Check existence of referenced readme files""" readmes = [readme] if isinstance(readme, str) else readme errors = [] for name in readmes: if not name: errors.append("Declared README file is an empty string.") elif not (poetry_file.parent / name).exists(): errors.append(f"Declared README file does not exist: {name}") return errors def _validate_dependencies_source(self, config: dict[str, Any]) -> list[str]: """Check dependencies's source are valid""" sources = {repository.name for repository in self.poetry.pool.all_repositories} dependency_declarations: list[ dict[str, str | dict[str, str] | list[dict[str, str]]] ] = [] # scan dependencies and group dependencies settings in pyproject.toml if "dependencies" in config: dependency_declarations.append(config["dependencies"]) for group in config.get("group", {}).values(): if "dependencies" in group: dependency_declarations.append(group["dependencies"]) all_referenced_sources: set[str] = set() for dependency_declaration in dependency_declarations: for declaration in dependency_declaration.values(): if isinstance(declaration, list): for item in declaration: if "source" in item: all_referenced_sources.add(item["source"]) elif isinstance(declaration, dict) and "source" in declaration: all_referenced_sources.add(declaration["source"]) return [ f'Invalid source "{source}" referenced in dependencies.' for source in sorted(all_referenced_sources - sources) ] def handle(self) -> int: from poetry.core.pyproject.toml import PyProjectTOML from poetry.factory import Factory # Load poetry config and display errors, if any poetry_file = self.poetry.file.path toml_data = PyProjectTOML(poetry_file).data check_result = Factory.validate(toml_data, strict=True) project = toml_data.get("project", {}) poetry_config = toml_data["tool"]["poetry"] # Validate trove classifiers project_classifiers = set( project.get("classifiers") or poetry_config.get("classifiers", []) ) errors, warnings = self._validate_classifiers(project_classifiers) check_result["errors"].extend(errors) check_result["warnings"].extend(warnings) readme_errors = [] # Check poetry readme if "readme" in poetry_config: readme_errors += self._validate_readme(poetry_config["readme"], poetry_file) project_readme = project.get("readme") if project_readme is not None: if isinstance(project_readme, dict): readme_path = project_readme.get("file") if readme_path is not None: readme_errors += self._validate_readme(readme_path, poetry_file) elif isinstance(project_readme, str): readme_errors += self._validate_readme(project_readme, poetry_file) else: # should not happen due to prior schema validation, but just in case readme_errors.append( f"Invalid format for [project.readme]: {project_readme!r}" ) check_result["errors"].extend(readme_errors) # Validate dependencies' sources check_result["errors"] += self._validate_dependencies_source(poetry_config) # Verify that lock file is consistent if self.option("lock") and not self.poetry.locker.is_locked(): check_result["errors"] += ["poetry.lock was not found."] if self.poetry.locker.is_locked() and not self.poetry.locker.is_fresh(): check_result["errors"] += [ "pyproject.toml changed significantly since poetry.lock was last generated. " "Run `poetry lock` to fix the lock file." ] return_code = 0 if check_result["errors"] or ( check_result["warnings"] and self.option("strict") ): return_code = 1 if not check_result["errors"] and not check_result["warnings"]: self.info("All set!") for error in check_result["errors"]: self.line_error(f"Error: {error}") for error in check_result["warnings"]: self.line_error(f"Warning: {error}") return return_code ================================================ FILE: src/poetry/console/commands/command.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from cleo.commands.command import Command as BaseCommand from cleo.exceptions import CleoValueError if TYPE_CHECKING: from poetry.console.application import Application from poetry.poetry import Poetry class Command(BaseCommand): loggers: ClassVar[list[str]] = [] _poetry: Poetry | None = None @property def poetry(self) -> Poetry: if self._poetry is None: return self.get_application().poetry return self._poetry def set_poetry(self, poetry: Poetry) -> None: """Explicitly set the current Poetry. Useful for Plugins that extends the features of a Poetry CLI Command. """ self._poetry = poetry def get_application(self) -> Application: from poetry.console.application import Application application = self.application assert isinstance(application, Application) return application def reset_poetry(self) -> None: self.get_application().reset_poetry() def option(self, name: str, default: Any = None) -> Any: try: return super().option(name) except CleoValueError: return default ================================================ FILE: src/poetry/console/commands/config.py ================================================ from __future__ import annotations import json import re from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from typing import cast from cleo.helpers import argument from cleo.helpers import option from installer.utils import canonicalize_name from poetry.config.config import PackageFilterPolicy from poetry.config.config import boolean_normalizer from poetry.config.config import boolean_validator from poetry.config.config import build_config_setting_normalizer from poetry.config.config import build_config_setting_validator from poetry.config.config import int_normalizer from poetry.config.config_source import UNSET from poetry.config.config_source import ConfigSourceMigration from poetry.config.config_source import PropertyNotFoundError from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from poetry.config.config_source import ConfigSource CONFIG_MIGRATIONS = [ ConfigSourceMigration( old_key="experimental.system-git-client", new_key="system-git-client" ), ConfigSourceMigration( old_key="virtualenvs.prefer-active-python", new_key="virtualenvs.use-poetry-python", value_migration={True: UNSET, False: True}, ), ] class ConfigCommand(Command): name = "config" description = "Manages configuration settings." arguments: ClassVar[list[Argument]] = [ argument("key", "Setting key.", optional=True), argument("value", "Setting value.", optional=True, multiple=True), ] options: ClassVar[list[Option]] = [ option("list", None, "List configuration settings."), option("unset", None, "Unset configuration setting."), option("local", None, "Set/Get from the project's local configuration."), option("migrate", None, "Migrate outdated configuration settings."), ] help = """\ This command allows you to edit the poetry config settings and repositories. To add a repository: poetry config repositories.foo https://bar.com/simple/ To remove a repository (repo is a short alias for repositories): poetry config --unset repo.foo""" LIST_PROHIBITED_SETTINGS: ClassVar[set[str]] = {"http-basic", "pypi-token"} @property def unique_config_values(self) -> dict[str, tuple[Any, Any]]: unique_config_values = { "cache-dir": (str, lambda val: str(Path(val))), "data-dir": (str, lambda val: str(Path(val))), "virtualenvs.create": (boolean_validator, boolean_normalizer), "virtualenvs.in-project": (boolean_validator, boolean_normalizer), "virtualenvs.options.always-copy": (boolean_validator, boolean_normalizer), "virtualenvs.options.system-site-packages": ( boolean_validator, boolean_normalizer, ), "virtualenvs.options.no-pip": (boolean_validator, boolean_normalizer), "virtualenvs.path": (str, lambda val: str(Path(val))), "virtualenvs.use-poetry-python": (boolean_validator, boolean_normalizer), "virtualenvs.prompt": (str, str), "system-git-client": (boolean_validator, boolean_normalizer), "requests.max-retries": (lambda val: int(val) >= 0, int_normalizer), "installer.re-resolve": (boolean_validator, boolean_normalizer), "installer.parallel": (boolean_validator, boolean_normalizer), "installer.max-workers": (lambda val: int(val) > 0, int_normalizer), "installer.no-binary": ( PackageFilterPolicy.validator, PackageFilterPolicy.normalize, ), "installer.only-binary": ( PackageFilterPolicy.validator, PackageFilterPolicy.normalize, ), "solver.lazy-wheel": (boolean_validator, boolean_normalizer), "keyring.enabled": (boolean_validator, boolean_normalizer), "python.installation-dir": (str, lambda val: str(Path(val))), } return unique_config_values def handle(self) -> int: from pathlib import Path from poetry.core.pyproject.exceptions import PyProjectError from poetry.config.config import Config from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR from poetry.toml.file import TOMLFile if self.option("migrate"): self._migrate() config = Config.create() config_file = TOMLFile(CONFIG_DIR / "config.toml") try: local_config_file = TOMLFile(self.poetry.file.path.parent / "poetry.toml") if local_config_file.exists(): config.merge(local_config_file.read()) except (RuntimeError, PyProjectError): local_config_file = TOMLFile(Path.cwd() / "poetry.toml") if self.option("local"): config.set_config_source(FileConfigSource(local_config_file)) if not config_file.exists(): config_file.path.parent.mkdir(parents=True, exist_ok=True) config_file.path.touch(mode=0o0600) if self.option("list"): self._list_configuration(config.all(), config.raw()) return 0 setting_key = self.argument("key") if not setting_key: return 0 if self.argument("value") and self.option("unset"): raise RuntimeError("You can not combine a setting value with --unset") # show the value if no value is provided if not self.argument("value") and not self.option("unset"): if setting_key.split(".")[0] in self.LIST_PROHIBITED_SETTINGS: raise ValueError(f"Expected a value for {setting_key} setting.") value: str | dict[str, Any] | list[str] if m := re.match( r"installer\.build-config-settings(\.([^.]+))?", self.argument("key") ): if not m.group(1): if value := config.get("installer.build-config-settings"): self._list_configuration(value, value) else: self.line("No packages configured with build config settings.") else: package_name = canonicalize_name(m.group(2)) key = f"installer.build-config-settings.{package_name}" if value := config.get(key): self.line(json.dumps(value)) else: self.line( f"No build config settings configured for {package_name}." ) return 0 elif m := re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")): if not m.group(1): value = {} if config.get("repositories") is not None: value = config.get("repositories") else: repo = config.get(f"repositories.{m.group(1)}") if repo is None: raise ValueError(f"There is no {m.group(1)} repository defined") value = repo self.line(str(value)) else: if setting_key not in self.unique_config_values: raise ValueError(f"There is no {setting_key} setting.") value = config.get(setting_key) if not isinstance(value, str): value = json.dumps(value) self.line(value) return 0 values: list[str] = self.argument("value") if setting_key in self.unique_config_values: if self.option("unset"): config.config_source.remove_property(setting_key) return 0 return self._handle_single_value( config.config_source, setting_key, self.unique_config_values[setting_key], values, ) # handle repositories m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) if m: if not m.group(1): raise ValueError("You cannot remove the [repositories] section") if self.option("unset"): repo = config.get(f"repositories.{m.group(1)}") if repo is None: raise ValueError(f"There is no {m.group(1)} repository defined") config.config_source.remove_property(f"repositories.{m.group(1)}") return 0 if len(values) == 1: url = values[0] config.config_source.add_property(f"repositories.{m.group(1)}.url", url) return 0 raise ValueError( "You must pass the url. " "Example: poetry config repositories.foo https://bar.com" ) # handle auth m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key")) if m: from poetry.utils.password_manager import PasswordManager password_manager = PasswordManager(config) if self.option("unset"): if m.group(1) == "http-basic": password_manager.delete_http_password(m.group(2)) elif m.group(1) == "pypi-token": password_manager.delete_pypi_token(m.group(2)) return 0 if m.group(1) == "http-basic": if len(values) == 1: username = values[0] # Only username, so we prompt for password password = self.secret("Password:") assert isinstance(password, str) elif len(values) != 2: raise ValueError( "Expected one or two arguments " f"(username, password), got {len(values)}" ) else: username = values[0] password = values[1] password_manager.set_http_password(m.group(2), username, password) elif m.group(1) == "pypi-token": if len(values) != 1: raise ValueError( f"Expected only one argument (token), got {len(values)}" ) token = values[0] password_manager.set_pypi_token(m.group(2), token) return 0 # handle certs m = re.match(r"certificates\.([^.]+)\.(cert|client-cert)", self.argument("key")) if m: repository = m.group(1) key = m.group(2) if self.option("unset"): config.auth_config_source.remove_property( f"certificates.{repository}.{key}" ) return 0 if len(values) == 1: new_value: str | bool = values[0] if key == "cert" and boolean_validator(values[0]): new_value = boolean_normalizer(values[0]) config.auth_config_source.add_property( f"certificates.{repository}.{key}", new_value ) else: raise ValueError("You must pass exactly 1 value") return 0 # handle build config settings m = re.match(r"installer\.build-config-settings\.([^.]+)", self.argument("key")) if m: key = f"installer.build-config-settings.{canonicalize_name(m.group(1))}" if self.option("unset"): config.config_source.remove_property(key) return 0 try: settings = config.config_source.get_property(key) except PropertyNotFoundError: settings = {} for value in values: if build_config_setting_validator(value): config_settings = build_config_setting_normalizer(value) for setting_name, item in config_settings.items(): settings[setting_name] = item else: raise ValueError( f"Invalid build config setting '{value}'. " "It must be a valid JSON with each property a string or a list of strings." ) config.config_source.add_property(key, settings) return 0 raise ValueError(f"Setting {self.argument('key')} does not exist") def _handle_single_value( self, source: ConfigSource, key: str, callbacks: tuple[Any, Any], values: list[Any], ) -> int: validator, normalizer = callbacks if len(values) > 1: raise RuntimeError("You can only pass one value.") value = values[0] if not validator(value): raise RuntimeError(f'"{value}" is an invalid value for {key}') source.add_property(key, normalizer(value)) return 0 def _list_configuration( self, config: dict[str, Any], raw: dict[str, Any], k: str = "" ) -> None: orig_k = k for key, value in sorted(config.items()): if k + key in self.LIST_PROHIBITED_SETTINGS: continue raw_val = raw.get(key) if isinstance(value, dict): k += f"{key}." raw_val = cast("dict[str, Any]", raw_val) self._list_configuration(value, raw_val, k=k) k = orig_k continue elif isinstance(value, list): value = ", ".join( json.dumps(val) if isinstance(val, list) else val for val in value ) value = f"[{value}]" if k.startswith("repositories."): message = f"{k + key} = {json.dumps(raw_val)}" elif isinstance(raw_val, str) and raw_val != value: message = ( f"{k + key} = {json.dumps(raw_val)} # {value}" ) else: message = f"{k + key} = {json.dumps(value)}" self.line(message) def _migrate(self) -> None: from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR from poetry.toml.file import TOMLFile config_file = TOMLFile(CONFIG_DIR / "config.toml") if self.option("local"): config_file = TOMLFile(self.poetry.file.path.parent / "poetry.toml") if not config_file.exists(): raise RuntimeError("No local config file found") config_source = FileConfigSource(config_file) self.io.write_line("Checking for required migrations ...") required_migrations = [ migration for migration in CONFIG_MIGRATIONS if migration.dry_run(config_source, io=self.io) ] if not required_migrations: self.io.write_line("Already up to date.") return if not self.io.is_interactive() or self.confirm( "Proceed with migration?: ", False ): for migration in required_migrations: migration.apply(config_source) self.io.write_line("Config migration successfully done.") ================================================ FILE: src/poetry/console/commands/debug/__init__.py ================================================ ================================================ FILE: src/poetry/console/commands/debug/info.py ================================================ from __future__ import annotations import sys from pathlib import Path from poetry.console.commands.command import Command class DebugInfoCommand(Command): name = "debug info" description = "Shows debug information." def handle(self) -> int: poetry_python_version = ".".join(str(s) for s in sys.version_info[:3]) self.line("") self.line("Poetry") self.line( "\n".join( [ f"Version: {self.poetry.VERSION}", f"Python: {poetry_python_version}", f"Path: {Path(sys.prefix)}", f"Executable: {Path(sys.executable) if sys.executable else 'Unknown'}", ] ) ) command = self.get_application().get("env info") exit_code: int = command.run(self.io) return exit_code ================================================ FILE: src/poetry/console/commands/debug/resolve.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from cleo.io.outputs.output import Verbosity from poetry.console.commands.init import InitCommand from poetry.console.commands.show import ShowCommand if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from cleo.ui.table import Rows class DebugResolveCommand(InitCommand): name = "debug resolve" description = "Debugs dependency resolution." arguments: ClassVar[list[Argument]] = [ argument("package", "The packages to resolve.", optional=True, multiple=True) ] options: ClassVar[list[Option]] = [ option( "extras", "E", "Extras to activate for the dependency.", flag=False, multiple=True, ), option("python", None, "Python version(s) to use for resolution.", flag=False), option("tree", None, "Display the dependency tree."), option("install", None, "Show what would be installed for the current system."), ] loggers: ClassVar[list[str]] = [ "poetry.repositories.pypi_repository", "poetry.inspection.info", ] def handle(self) -> int: from cleo.io.null_io import NullIO from poetry.core.packages.project_package import ProjectPackage from poetry.factory import Factory from poetry.puzzle.solver import Solver from poetry.repositories.repository import Repository from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import EnvManager packages = self.argument("package") if not packages: package = self.poetry.package else: # Using current pool for determine_requirements() self._pool = self.poetry.pool package = ProjectPackage( self.poetry.package.name, self.poetry.package.version ) # Silencing output verbosity = self.io.output.verbosity self.io.output.set_verbosity(Verbosity.QUIET) requirements = self._determine_requirements(packages) self.io.output.set_verbosity(verbosity) for constraint in requirements: name = constraint.pop("name") assert isinstance(name, str) extras = [] for extra in self.option("extras"): extras += extra.split() constraint["extras"] = extras package.add_dependency(Factory.create_dependency(name, constraint)) package.python_versions = self.option("python") or ( self.poetry.package.python_versions ) pool = self.poetry.pool solver = Solver(package, pool, [], [], self.io) ops = solver.solve().calculate_operations() self.line("") self.line("Resolution results:") self.line("") if self.option("tree"): show_command = self.get_application().find("show") assert isinstance(show_command, ShowCommand) show_command.init_styles(self.io) packages = [op.package for op in ops] requires = package.all_requires for pkg in packages: for require in requires: if pkg.name == require.name: show_command.display_package_tree(self.io, pkg, packages) break return 0 table = self.table(style="compact") table.style.set_vertical_border_chars("", " ") rows: Rows = [] if self.option("install"): env = EnvManager(self.poetry).get() pool = RepositoryPool(config=self.poetry.config) locked_repository = Repository("poetry-locked") for op in ops: locked_repository.add_package(op.package) pool.add_repository(locked_repository) solver = Solver(package, pool, [], [], NullIO()) with solver.use_environment(env): ops = solver.solve().calculate_operations() for op in ops: if self.option("install") and op.skipped: continue pkg = op.package row = [ f"{pkg.complete_name}", f"{pkg.version}", ] if not pkg.marker.is_any(): row[2] = str(pkg.marker) rows.append(row) table.set_rows(rows) table.render() return 0 ================================================ FILE: src/poetry/console/commands/debug/tags.py ================================================ from __future__ import annotations from poetry.console.commands.env_command import EnvCommand class DebugTagsCommand(EnvCommand): name = "debug tags" description = "Shows compatible tags for your project's current active environment." def handle(self) -> int: for tag in self.env.get_supported_tags(): self.io.write_line( f"{tag.interpreter}" f"-{tag.abi}" f"-{tag.platform}" ) return 0 ================================================ FILE: src/poetry/console/commands/env/__init__.py ================================================ ================================================ FILE: src/poetry/console/commands/env/activate.py ================================================ from __future__ import annotations import shlex from typing import TYPE_CHECKING import shellingham from poetry.console.commands.env_command import EnvCommand from poetry.utils._compat import WINDOWS if TYPE_CHECKING: from pathlib import Path from poetry.utils.env import Env class ShellNotSupportedError(Exception): """Raised when a shell doesn't have an activator in virtual environment""" class EnvActivateCommand(EnvCommand): name = "env activate" description = "Print the command to activate a virtual environment." def handle(self) -> int: from poetry.utils.env import EnvManager env = EnvManager(self.poetry).get() try: shell, _ = shellingham.detect_shell() except shellingham.ShellDetectionFailure: shell = "" if command := self._get_activate_command(env, shell): self.line(command) return 0 raise ShellNotSupportedError( f"Discovered shell '{shell}' doesn't have an activator in virtual environment" ) def _get_activate_command(self, env: Env, shell: str) -> str: if shell == "fish": command, filename = "source", "activate.fish" elif shell == "nu": command, filename = "overlay use", "activate.nu" elif shell in ["csh", "tcsh"]: command, filename = "source", "activate.csh" elif shell in ["powershell", "pwsh"]: command, filename = "&", "activate.ps1" elif shell == "cmd": command, filename = "", "activate.bat" elif shell in ["bash", "mksh", "zsh"]: command, filename = "source", "activate" else: command, filename = ".", "activate" if (activation_script := env.bin_dir / filename).exists(): quoted = self._quote(activation_script, shell) return f"{command} {quoted}".strip() return "" @staticmethod def _quote(activation_script: Path, shell: str) -> str: if WINDOWS and shell in {"cmd", "powershell", "pwsh"}: return f'"{activation_script}"' return shlex.quote(activation_script.as_posix()) ================================================ FILE: src/poetry/console/commands/env/info.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import option from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.option import Option from poetry.utils.env import Env class EnvInfoCommand(Command): name = "env info" description = "Displays information about the current environment." options: ClassVar[list[Option]] = [ option("path", "p", "Only display the environment's path."), option( "executable", "e", "Only display the environment's python executable path." ), ] def handle(self) -> int: from poetry.utils.env import EnvManager env = EnvManager(self.poetry).get() if self.option("path"): if not env.is_venv(): return 1 self.line(str(env.path)) return 0 if self.option("executable"): if not env.is_venv(): return 1 self.line(str(env.python)) return 0 self._display_complete_info(env) return 0 def _display_complete_info(self, env: Env) -> None: env_python_version = ".".join(str(s) for s in env.version_info[:3]) self.line("") self.line("Virtualenv") listing = [ f"Python: {env_python_version}", f"Implementation: {env.python_implementation}", ( "Path: " f" {env.path if env.is_venv() else 'NA'}" ), ( "Executable: " f" {env.python if env.is_venv() else 'NA'}" ), ] if env.is_venv(): listing.append( "Valid: " f" <{'comment' if env.is_sane() else 'error'}>{env.is_sane()}" ) self.line("\n".join(listing)) self.line("") base_env = env.parent_env python = ".".join(str(v) for v in base_env.version_info[:3]) self.line("Base") self.line( "\n".join( [ f"Platform: {env.platform}", f"OS: {env.os}", f"Python: {python}", f"Path: {base_env.path}", f"Executable: {base_env.python}", ] ) ) ================================================ FILE: src/poetry/console/commands/env/list.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import option from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.option import Option class EnvListCommand(Command): name = "env list" description = "Lists all virtualenvs associated with the current project." options: ClassVar[list[Option]] = [ option("full-path", None, "Output the full paths of the virtualenvs.") ] def handle(self) -> int: from poetry.utils.env import EnvManager manager = EnvManager(self.poetry) current_env = manager.get() for venv in manager.list(): name = venv.path.name if self.option("full-path"): name = str(venv.path) if venv == current_env: self.line(f"{name} (Activated)") continue self.line(name) return 0 ================================================ FILE: src/poetry/console/commands/env/remove.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class EnvRemoveCommand(Command): name = "env remove" description = "Remove virtual environments associated with the project." arguments: ClassVar[list[Argument]] = [ argument( "python", "The python executables associated with, or names of the virtual" " environments which are to be removed.", optional=True, multiple=True, ) ] options: ClassVar[list[Option]] = [ option( "all", description=( "Remove all managed virtual environments associated with the project." ), ), ] def handle(self) -> int: from poetry.utils.env import EnvManager is_in_project = self.poetry.config.get("virtualenvs.in-project") pythons = self.argument("python") remove_all_envs = self.option("all") if not (pythons or remove_all_envs or is_in_project): self.line("No virtualenv provided.") manager = EnvManager(self.poetry) # TODO: refactor env.py to allow removal with one loop for python in pythons: venv = manager.remove(python) self.line(f"Deleted virtualenv: {venv.path}") if remove_all_envs or is_in_project: for venv in manager.list(): if not is_in_project or venv.path.is_relative_to( self.poetry.pyproject_path.parent ): manager.remove_venv(venv.path) self.line(f"Deleted virtualenv: {venv.path}") # Since we remove all the virtualenvs, we can also remove the entry # in the envs file. (Strictly speaking, we should do this explicitly, # in case it points to a virtualenv that had been removed manually before.) if remove_all_envs and manager.envs_file.exists(): manager.envs_file.remove_section(manager.base_env_name) return 0 ================================================ FILE: src/poetry/console/commands/env/use.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.argument import Argument class EnvUseCommand(Command): name = "env use" description = "Activates or creates a new virtualenv for the current project." arguments: ClassVar[list[Argument]] = [ argument("python", "The python executable to use.") ] def handle(self) -> int: from poetry.utils.env import EnvManager manager = EnvManager(self.poetry, io=self.io) if self.argument("python") == "system": manager.deactivate() return 0 env = manager.activate(self.argument("python")) self.line(f"Using virtualenv: {env.path}") return 0 ================================================ FILE: src/poetry/console/commands/env_command.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.console.commands.command import Command if TYPE_CHECKING: from poetry.utils.env import Env class EnvCommand(Command): def __init__(self) -> None: # Set in poetry.console.application.Application.configure_env self._env: Env | None = None super().__init__() @property def env(self) -> Env: assert self._env is not None return self._env def set_env(self, env: Env) -> None: self._env = env ================================================ FILE: src/poetry/console/commands/group_command.py ================================================ from __future__ import annotations from collections import defaultdict from typing import TYPE_CHECKING from cleo.helpers import option from packaging.utils import NormalizedName from packaging.utils import canonicalize_name from poetry.console.commands.command import Command from poetry.console.exceptions import GroupNotFoundError if TYPE_CHECKING: from cleo.io.inputs.option import Option from poetry.core.packages.project_package import ProjectPackage class GroupCommand(Command): @staticmethod def _group_dependency_options() -> list[Option]: return [ option( "without", None, "The dependency groups to ignore.", flag=False, multiple=True, ), option( "with", None, "The optional dependency groups to include.", flag=False, multiple=True, ), option( "only", None, "The only dependency groups to include.", flag=False, multiple=True, ), ] @property def non_optional_groups(self) -> set[str]: # TODO: this should move into poetry-core return { group.name for group in self.poetry.package._dependency_groups.values() if not group.is_optional() } @property def default_group(self) -> str | None: """ The default group to use when no group is specified. This is useful for command that have the `--group` option, eg: add, remove. Can be overridden to adapt behavior. """ return None @property def default_groups(self) -> set[str]: """ The groups that are considered by the command by default. Can be overridden to adapt behavior. """ return self.non_optional_groups @property def activated_groups(self) -> set[NormalizedName]: groups = {} for key in {"with", "without", "only"}: groups[key] = { group.strip() for groups in self.option(key, "") for group in groups.split(",") } if self.option("all-groups"): groups["with"] = self.poetry.package.dependency_group_names( include_optional=True ) self._validate_group_options(groups) if groups["only"] and (groups["with"] or groups["without"]): self.line_error( "The `--with` and " "`--without` options are ignored when used" " along with the `--only` option." "" ) # Normalize after validating so that original names are printed # in case of an error. norm_groups = { key: {canonicalize_name(group) for group in key_groups} for key, key_groups in groups.items() } norm_default_groups = {canonicalize_name(name) for name in self.default_groups} return norm_groups["only"] or norm_default_groups.union( norm_groups["with"] ).difference(norm_groups["without"]) def project_with_activated_groups_only(self) -> ProjectPackage: return self.poetry.package.with_dependency_groups( list(self.activated_groups), only=True ) def _validate_group_options(self, group_options: dict[str, set[str]]) -> None: """ Raises an error if it detects that a group is not part of pyproject.toml """ invalid_options = defaultdict(set) for opt, groups in group_options.items(): for group in groups: if not self.poetry.package.has_dependency_group(group): invalid_options[group].add(opt) if invalid_options: message_parts = [] for group in sorted(invalid_options): opts = ", ".join( f"--{opt}" for opt in sorted(invalid_options[group]) ) message_parts.append(f"{group} (via {opts})") raise GroupNotFoundError(f"Group(s) not found: {', '.join(message_parts)}") ================================================ FILE: src/poetry/console/commands/init.py ================================================ from __future__ import annotations import re from collections.abc import Mapping from contextlib import suppress from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from cleo.helpers import option from packaging.utils import canonicalize_name from tomlkit import inline_table from poetry.console.commands.command import Command from poetry.console.commands.env_command import EnvCommand from poetry.utils.dependency_specification import RequirementsParser from poetry.utils.env.python import Python if TYPE_CHECKING: from cleo.io.inputs.option import Option from packaging.utils import NormalizedName from poetry.core.packages.package import Package from tomlkit.items import InlineTable from poetry.repositories import RepositoryPool Requirements = dict[str, str | Mapping[str, Any]] class InitCommand(Command): name = "init" description = ( "Creates a basic pyproject.toml file in the current directory." ) options: ClassVar[list[Option]] = [ option("name", None, "Name of the package.", flag=False), option("description", None, "Description of the package.", flag=False), option("author", None, "Author name of the package.", flag=False), option("python", None, "Compatible Python versions.", flag=False), option( "dependency", None, "Package to require, with an optional version constraint, " "e.g. requests:^2.10.0 or requests=2.11.1.", flag=False, multiple=True, ), option( "dev-dependency", None, "Package to require for development, with an optional version" " constraint, e.g. requests:^2.10.0 or requests=2.11.1.", flag=False, multiple=True, ), option("license", "l", "License of the package.", flag=False), ] help = """\ The init command creates a basic pyproject.toml file in the\ current directory. """ def __init__(self) -> None: super().__init__() self._pool: RepositoryPool | None = None def handle(self) -> int: from pathlib import Path project_path = Path.cwd() if self.io.input.option("project"): project_path = Path(self.io.input.option("project")) if not project_path.exists() or not project_path.is_dir(): self.line_error("The --project path is not a directory.") return 1 return self._init_pyproject(project_path=project_path) def _init_pyproject( self, project_path: Path, allow_interactive: bool = True, layout_name: str = "standard", readme_format: str = "md", allow_layout_creation_on_empty: bool = False, ) -> int: from poetry.core.vcs.git import GitConfig from poetry.config.config import Config from poetry.layouts import layout from poetry.pyproject.toml import PyProjectTOML is_interactive = self.io.is_interactive() and allow_interactive pyproject = PyProjectTOML(project_path / "pyproject.toml") if pyproject.file.exists(): if pyproject.is_poetry_project(): self.line_error( "A pyproject.toml file with a project and/or" " a poetry section already exists." ) return 1 if pyproject.data.get("build-system"): self.line_error( "A pyproject.toml file with a defined build-system already" " exists." ) return 1 vcs_config = GitConfig() if is_interactive: self.line("") self.line( "This command will guide you through creating your" " pyproject.toml config." ) self.line("") name = self.option("name") if not name: name = project_path.name.lower() if is_interactive: question = self.create_question( f"Package name [{name}]: ", default=name ) name = self.ask(question) version = "0.1.0" if is_interactive: question = self.create_question( f"Version [{version}]: ", default=version ) version = self.ask(question) description = self.option("description") or "" if not description and is_interactive: description = self.ask(self.create_question("Description []: ", default="")) author = self.option("author") if not author and vcs_config.get("user.name"): author = vcs_config["user.name"] author_email = vcs_config.get("user.email") if author_email: author += f" <{author_email}>" if is_interactive: question = self.create_question( f"Author [{author}, n to skip]: ", default=author ) question.set_validator(lambda v: self._validate_author(v, author)) author = self.ask(question) authors = [author] if author else [] license_name = self.option("license") if not license_name and is_interactive: license_name = self.ask(self.create_question("License []: ", default="")) python = self.option("python") if not python: config = Config.create() python = ( ">=" + Python.get_preferred_python(config, self.io).minor_version.to_string() ) if is_interactive: question = self.create_question( f"Compatible Python versions [{python}]: ", default=python, ) python = self.ask(question) if is_interactive: self.line("") requirements: Requirements = {} if self.option("dependency"): requirements = self._format_requirements( self._determine_requirements(self.option("dependency")) ) question_text = "Would you like to define your main dependencies interactively?" help_message = """\ You can specify a package in the following forms: - A single name (requests): this will search for matches on PyPI - A name and a constraint (requests@^2.23.0) - A git url (git+https://github.com/python-poetry/poetry.git) - A git url with a revision\ (git+https://github.com/python-poetry/poetry.git#develop) - A file path (../my-package/my-package.whl) - A directory (../my-package/) - A url (https://example.com/packages/my-package-0.1.0.tar.gz) """ help_displayed = False if is_interactive and self.confirm(question_text, True): self.line(help_message) help_displayed = True requirements.update( self._format_requirements(self._determine_requirements([])) ) self.line("") dev_requirements: Requirements = {} if self.option("dev-dependency"): dev_requirements = self._format_requirements( self._determine_requirements(self.option("dev-dependency")) ) question_text = ( "Would you like to define your development dependencies interactively?" ) if is_interactive and self.confirm(question_text, True): if not help_displayed: self.line(help_message) dev_requirements.update( self._format_requirements(self._determine_requirements([])) ) self.line("") layout_ = layout(layout_name)( name, version, description=description, author=authors[0] if authors else None, readme_format=readme_format, license=license_name, python=python, dependencies=requirements, dev_dependencies=dev_requirements, ) create_layout = not project_path.exists() or ( allow_layout_creation_on_empty and not any(project_path.iterdir()) ) if create_layout: layout_.create(project_path, with_pyproject=False) content = layout_.generate_project_content(project_path) for section, item in content.items(): pyproject.data.append(section, item) if is_interactive: self.line("Generated file") self.line("") self.line(pyproject.data.as_string().replace("\r\n", "\n")) self.line("") if is_interactive and not self.confirm("Do you confirm generation?", True): self.line_error("Command aborted") return 1 pyproject.save() if create_layout: path = project_path.resolve() with suppress(ValueError): path = path.relative_to(Path.cwd()) self.line( f"Created package {layout_._package_name} in" f" {path.as_posix()}" ) return 0 def _generate_choice_list( self, matches: list[Package], canonicalized_name: NormalizedName ) -> list[str]: choices = [] matches_names = [p.name for p in matches] exact_match = canonicalized_name in matches_names if exact_match: choices.append(matches[matches_names.index(canonicalized_name)].pretty_name) for found_package in matches: if len(choices) >= 10: break if found_package.name == canonicalized_name: continue choices.append(found_package.pretty_name) return choices def _determine_requirements( self, requires: list[str], allow_prereleases: bool | None = None, source: str | None = None, is_interactive: bool | None = None, ) -> list[dict[str, Any]]: if is_interactive is None: is_interactive = self.io.is_interactive() if not requires: result = [] question = self.create_question( "Package to add or search for (leave blank to skip):" ) question.set_validator(self._validate_package) follow_up_question = self.create_question( "\nAdd a package (leave blank to skip):" ) follow_up_question.set_validator(self._validate_package) package = self.ask(question) while package: constraint = self._parse_requirements([package])[0] if ( "git" in constraint or "url" in constraint or "path" in constraint or "version" in constraint ): self.line(f"Adding {package}") result.append(constraint) package = self.ask(follow_up_question) continue canonicalized_name = canonicalize_name(constraint["name"]) matches = self._get_pool().search(canonicalized_name) if not matches: self.line_error("Unable to find package") package = False else: choices = self._generate_choice_list(matches, canonicalized_name) info_string = ( f"Found {len(matches)} packages matching" f" {package}" ) if len(matches) > 10: info_string += "\nShowing the first 10 matches" self.line(info_string) # Default to an empty value to signal no package was selected choices.append("") package = self.choice( "\nEnter package # to add, or the complete package name if" " it is not listed", choices, attempts=3, default=len(choices) - 1, ) if not package: self.line("No package selected") # package selected by user, set constraint name to package name if package: constraint["name"] = package # no constraint yet, determine the best version automatically if package and "version" not in constraint: question = self.create_question( "Enter the version constraint to require " "(or leave blank to use the latest version):" ) question.set_max_attempts(3) question.set_validator(lambda x: (x or "").strip() or None) package_constraint = self.ask(question) if package_constraint is None: _, package_constraint = self._find_best_version_for_package( package ) self.line( f"Using version {package_constraint} for" f" {package}" ) constraint["version"] = package_constraint if package: result.append(constraint) if is_interactive: package = self.ask(follow_up_question) return result result = [] for requirement in self._parse_requirements(requires): if "git" in requirement or "url" in requirement or "path" in requirement: result.append(requirement) continue elif "version" not in requirement: # determine the best version automatically name, version = self._find_best_version_for_package( requirement["name"], allow_prereleases=allow_prereleases, source=source, ) requirement["version"] = version requirement["name"] = name self.line(f"Using version {version} for {name}") else: # check that the specified version/constraint exists # before we proceed name, _ = self._find_best_version_for_package( requirement["name"], requirement["version"], allow_prereleases=allow_prereleases, source=source, ) requirement["name"] = name result.append(requirement) return result def _find_best_version_for_package( self, name: str, required_version: str | None = None, allow_prereleases: bool | None = None, source: str | None = None, ) -> tuple[str, str]: from poetry.version.version_selector import VersionSelector selector = VersionSelector(self._get_pool()) package = selector.find_best_candidate( name, required_version, allow_prereleases=allow_prereleases, source=source ) if not package: # TODO: find similar raise ValueError(f"Could not find a matching version of package {name}") version = package.version.without_local() return package.pretty_name, f"^{version.to_string()}" def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: from poetry.core.pyproject.exceptions import PyProjectError try: cwd = self.poetry.file.path.parent artifact_cache = self.poetry.pool.artifact_cache except (PyProjectError, RuntimeError): cwd = Path.cwd() artifact_cache = self._get_pool().artifact_cache parser = RequirementsParser( artifact_cache=artifact_cache, env=self.env if isinstance(self, EnvCommand) else None, cwd=cwd, ) return [ parser.parse(re.sub(r"@\s*latest$", "", requirement, flags=re.I)) for requirement in requirements ] def _format_requirements(self, requirements: list[dict[str, str]]) -> Requirements: requires: Requirements = {} for requirement in requirements: name = requirement.pop("name") constraint: str | InlineTable if "version" in requirement and len(requirement) == 1: constraint = requirement["version"] else: constraint = inline_table() constraint.trivia.trail = "\n" constraint.update(requirement) requires[name] = constraint return requires @staticmethod def _validate_author(author: str, default: str) -> str | None: from poetry.core.utils.helpers import combine_unicode from poetry.core.utils.patterns import AUTHOR_REGEX author = combine_unicode(author or default) if author in ["n", "no"]: return None m = AUTHOR_REGEX.match(author) if not m: raise ValueError( "Invalid author string. Must be in the format: " "John Smith " ) return author @staticmethod def _validate_package(package: str | None) -> str | None: if package and len(package.split()) > 2: raise ValueError("Invalid package definition.") return package def _get_pool(self) -> RepositoryPool: from poetry.config.config import Config from poetry.repositories import RepositoryPool from poetry.repositories.pypi_repository import PyPiRepository if isinstance(self, EnvCommand): return self.poetry.pool if self._pool is None: self._pool = RepositoryPool() pool_size = Config.create().installer_max_workers self._pool.add_repository(PyPiRepository(pool_size=pool_size)) return self._pool ================================================ FILE: src/poetry/console/commands/install.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import option from poetry.console.commands.installer_command import InstallerCommand from poetry.plugins.plugin_manager import PluginManager if TYPE_CHECKING: from cleo.io.inputs.option import Option from packaging.utils import NormalizedName class InstallCommand(InstallerCommand): name = "install" description = "Installs the project dependencies." options: ClassVar[list[Option]] = [ *InstallerCommand._group_dependency_options(), option( "sync", None, "Synchronize the environment with the locked packages and the specified" " groups. (Deprecated)", ), option( "no-root", None, "Do not install the root package (the current project)." ), option( "no-directory", None, "Do not install any directory path dependencies; useful to install" " dependencies without source code, e.g. for caching of Docker layers)", flag=True, multiple=False, ), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), option( "extras", "E", "Extra sets of dependencies to install.", flag=False, multiple=True, ), option("all-extras", None, "Install all extra dependencies."), option("all-groups", None, "Install dependencies from all groups."), option("only-root", None, "Exclude all dependencies."), option( "compile", None, "Compile Python source files to bytecode.", ), ] help = """\ The install command reads the poetry.lock file from the current directory, processes it, and downloads and installs all the libraries and dependencies outlined in that file. If the file does not exist it will look for pyproject.toml and do the same. poetry install By default, the above command will also install the current project. To install only the dependencies and not including the current project, run the command with the --no-root option like below: poetry install --no-root If you want to use Poetry only for dependency management but not for packaging, you can set the "package-mode" to false in your pyproject.toml file. """ _loggers: ClassVar[list[str]] = [ "poetry.repositories.pypi_repository", "poetry.inspection.info", ] @property def activated_groups(self) -> set[NormalizedName]: if self.option("only-root"): return set() else: return super().activated_groups @property def _alternative_sync_command(self) -> str: return "poetry sync" @property def _with_synchronization(self) -> bool: with_synchronization = self.option("sync") if with_synchronization: self.line_error( "The `--sync` option is" " deprecated and slated for removal, use the" f" `{self._alternative_sync_command}`" " command instead." ) return bool(with_synchronization) def handle(self) -> int: from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError from poetry.masonry.builders.editable import EditableBuilder if not self.option("no-plugins"): PluginManager.ensure_project_plugins(self.poetry, self.io) if self.option("extras") and self.option("all-extras"): self.line_error( "You cannot specify explicit" " `--extras` while installing" " using `--all-extras`." ) return 1 if self.option("only-root") and any( self.option(key) for key in {"with", "without", "only", "all-groups"} ): self.line_error( "The `--with`," " `--without`," " `--only` and" " `--all-groups`" " options cannot be used with" " the `--only-root`" " option." ) return 1 if self.option("only-root") and self.option("no-root"): self.line_error( "You cannot specify `--no-root`" " when using `--only-root`." ) return 1 if ( self.option("only") or self.option("with") or self.option("without") ) and self.option("all-groups"): self.line_error( "You cannot specify `--with`," " `--without`, or" " `--only` when using" " `--all-groups`." ) return 1 extras: list[str] if self.option("all-extras"): extras = list(self.poetry.package.extras.keys()) else: extras = [] for extra in self.option("extras", []): extras += extra.split() self.installer.extras(extras) self.installer.only_groups(self.activated_groups) self.installer.skip_directory(self.option("no-directory")) self.installer.dry_run(self.option("dry-run")) self.installer.requires_synchronization(self._with_synchronization) self.installer.executor.enable_bytecode_compilation(self.option("compile")) self.installer.verbose(self.io.is_verbose()) return_code = self.installer.run() if return_code != 0: return return_code if self.option("no-root") or not self.poetry.is_package_mode: return 0 log_install = ( "Installing the current project:" f" {self.poetry.package.pretty_name}" f" (<{{tag}}>{self.poetry.package.pretty_version})" ) overwrite = self.io.output.is_decorated() and not self.io.is_debug() self.line("") self.write(log_install.format(tag="c2")) if not overwrite: self.line("") if self.option("dry-run"): self.line("") return 0 # Prior to https://github.com/python-poetry/poetry-core/pull/629 # the existence of a module/package was checked when creating the # EditableBuilder. Afterwards, the existence is checked after # executing the build script (if there is one), # i.e. during EditableBuilder.build(). try: builder = EditableBuilder(self.poetry, self.env, self.io) builder.build() except (ModuleOrPackageNotFoundError, FileNotFoundError) as e: # This is likely due to the fact that the project is an application # not following the structure expected by Poetry. # No need for an editable install in this case. self.line("") self.line_error( f"Error: The current project could not be installed: {e}\n" "If you do not want to install the current project" " use --no-root.\n" "If you want to use Poetry only for dependency management" " but not for packaging, you can disable package mode by setting" " package-mode = false in your pyproject.toml file.\n" "If you did intend to install the current project, you may need" " to set `packages` in your pyproject.toml file.\n", style="error", ) return 1 if overwrite: self.overwrite(log_install.format(tag="success")) self.line("") return 0 ================================================ FILE: src/poetry/console/commands/installer_command.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.console.commands.env_command import EnvCommand from poetry.console.commands.group_command import GroupCommand from poetry.utils.password_manager import PoetryKeyring if TYPE_CHECKING: from cleo.io.io import IO from poetry.installation.installer import Installer class InstallerCommand(GroupCommand, EnvCommand): def __init__(self) -> None: # Set in poetry.console.application.Application.configure_installer self._installer: Installer | None = None super().__init__() def reset_poetry(self) -> None: super().reset_poetry() self.installer.set_package(self.poetry.package) self.installer.set_locker(self.poetry.locker) @property def installer(self) -> Installer: assert self._installer is not None return self._installer def set_installer(self, installer: Installer) -> None: self._installer = installer def execute(self, io: IO) -> int: PoetryKeyring.preflight_check(io, self.poetry.config) return super().execute(io) ================================================ FILE: src/poetry/console/commands/lock.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import option from poetry.console.commands.installer_command import InstallerCommand if TYPE_CHECKING: from cleo.io.inputs.option import Option class LockCommand(InstallerCommand): name = "lock" description = "Locks the project dependencies." options: ClassVar[list[Option]] = [ option( "regenerate", None, "Ignore existing lock file" " and overwrite it with a new lock file created from scratch.", ), ] help = """ The lock command reads the pyproject.toml file from the current directory, processes it, and locks the dependencies in the\ poetry.lock file. By default, packages that have already been added to the lock file before will not be updated. poetry lock """ loggers: ClassVar[list[str]] = ["poetry.repositories.pypi_repository"] def handle(self) -> int: self.installer.lock(update=self.option("regenerate")) return self.installer.run() ================================================ FILE: src/poetry/console/commands/new.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from poetry.console.commands.init import InitCommand if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class NewCommand(InitCommand): name = "new" description = "Creates a new Python project at ." arguments: ClassVar[list[Argument]] = [ argument("path", "The path to create the project at.") ] options: ClassVar[list[Option]] = [ option( "interactive", "i", "Allow interactive specification of project configuration.", flag=True, ), option("name", None, "Set the resulting package name.", flag=False), option( "src", None, "Use the src layout for the project. " "Deprecated: This is the default option now.", ), option("flat", None, "Use the flat layout for the project."), option( "readme", None, "Specify the readme file format. Default is md.", flag=False, ), *[ o for o in InitCommand.options if o.name in { "description", "author", "python", "dependency", "dev-dependency", "license", } ], ] def handle(self) -> int: from pathlib import Path if self.io.input.option("project"): self.line_error( "--project only makes sense with existing projects, and will" " be ignored. You should consider the option --path instead." ) path = Path(self.argument("path")) if not path.is_absolute(): # we do not use resolve here due to compatibility issues # for path.resolve(strict=False) path = Path.cwd().joinpath(path) if path.exists() and list(path.glob("*")): # Directory is not empty. Aborting. raise RuntimeError( f"Destination {path} exists and is not empty. Did you mean `poetry init`?" ) if self.option("src"): self.line_error( "The --src option is now the default and will be removed in a future version." ) return self._init_pyproject( project_path=path, allow_interactive=self.option("interactive"), layout_name="standard" if self.option("flat") else "src", readme_format=self.option("readme") or "md", allow_layout_creation_on_empty=True, ) ================================================ FILE: src/poetry/console/commands/publish.py ================================================ from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import option from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.option import Option class PublishCommand(Command): name = "publish" description = "Publishes a package to a remote repository." options: ClassVar[list[Option]] = [ option( "repository", "r", "The repository to publish the package to.", flag=False ), option("username", "u", "The username to access the repository.", flag=False), option("password", "p", "The password to access the repository.", flag=False), option( "cert", None, "Certificate authority to access the repository.", flag=False ), option( "client-cert", None, "Client certificate to access the repository.", flag=False, ), option( "dist-dir", None, "Dist directory where built artifact are stored. Default is `dist`.", default="dist", flag=False, ), option("build", None, "Build the package before publishing."), option("dry-run", None, "Perform all actions except upload the package."), option( "skip-existing", None, "Ignore errors from files already existing in the repository.", ), ] help = """The publish command builds and uploads the package to a remote repository. By default, it will upload to PyPI but if you pass the --repository option it will upload to it instead. The --repository option should match the name of a configured repository using the config command. """ loggers: ClassVar[list[str]] = ["poetry.publishing.publisher"] def handle(self) -> int: from poetry.publishing.publisher import Publisher if not self.poetry.is_package_mode: self.line_error("Publishing a package is not possible in non-package mode.") return 1 dist_dir = self.option("dist-dir") publisher = Publisher(self.poetry, self.io, Path(dist_dir)) # Building package first, if told if self.option("build"): if publisher.files and not self.confirm( f"There are {len(publisher.files)} files ready for" " publishing. Build anyway?" ): self.line_error("Aborted!") return 1 self.call("build", args=f"--output {dist_dir}") publisher = Publisher(self.poetry, self.io, Path(dist_dir)) if not publisher.files: self.line_error( "No files to publish. " "Run poetry build first or use the --build option." ) return 1 self.line("") cert = Path(self.option("cert")) if self.option("cert") else None client_cert = ( Path(self.option("client-cert")) if self.option("client-cert") else None ) publisher.publish( self.option("repository"), self.option("username"), self.option("password"), cert, client_cert, self.option("dry-run"), self.option("skip-existing"), ) return 0 ================================================ FILE: src/poetry/console/commands/python/__init__.py ================================================ from __future__ import annotations def get_request_title(request: str, implementation: str, free_threaded: bool) -> str: add_info = implementation if free_threaded: add_info += ", free-threaded" return f"{request} ({add_info})" ================================================ FILE: src/poetry/console/commands/python/install.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from poetry.core.constraints.version.version import Version from poetry.core.version.exceptions import InvalidVersionError from poetry.console.commands.command import Command from poetry.console.commands.python import get_request_title from poetry.console.commands.python.remove import PythonRemoveCommand from poetry.console.exceptions import PoetryRuntimeError from poetry.utils.env.python.installer import PythonDownloadNotFoundError from poetry.utils.env.python.installer import PythonInstallationError from poetry.utils.env.python.installer import PythonInstaller from poetry.utils.env.python.providers import PoetryPythonPathProvider if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class PythonInstallCommand(Command): name = "python install" arguments: ClassVar[list[Argument]] = [ argument("python", "The python version to install.") ] options: ClassVar[list[Option]] = [ option("clean", "c", "Clean up installation if check fails.", flag=True), option( "free-threaded", "t", "Use free-threaded version if available.", flag=True ), option( "implementation", "i", "Python implementation to use. (cpython, pypy)", flag=False, default="cpython", ), option( "reinstall", "r", "Reinstall if installation already exists.", flag=True ), ] description = ( "Install the specified Python version from the Python Standalone Builds project." " (experimental feature)" ) def handle(self) -> int: request = self.argument("python") impl = self.option("implementation").lower() reinstall = self.option("reinstall") free_threaded = self.option("free-threaded") if request.endswith("t"): free_threaded = True request = request[:-1] try: version = Version.parse(request) except (ValueError, InvalidVersionError): self.io.write_error_line( f"Invalid Python version requested {request}" ) return 1 if free_threaded and version < Version.parse("3.13.0"): self.io.write_error_line("") self.io.write_error_line( "Free threading is not supported for Python versions prior to 3.13.0.\n\n" "See https://docs.python.org/3/howto/free-threading-python.html for more information." ) self.io.write_error_line("") return 1 installer = PythonInstaller(request, impl, free_threaded) try: if installer.exists() and not reinstall: self.io.write_error_line( "Python version already installed at " f"{PoetryPythonPathProvider.installation_dir(version, impl, free_threaded)}.\n" ) self.io.write_error_line( f"Use --reinstall to install anyway, " f"or use poetry python remove {version} first." ) return 1 except PythonDownloadNotFoundError: self.io.write_error_line( "No suitable standalone build found for the requested Python version." ) return 1 request_title = get_request_title(request, impl, free_threaded) try: self.io.write(f"Downloading and installing {request_title} ... ") installer.install() except PythonInstallationError as e: self.io.write("Failed\n") self.io.write_error_line("") self.io.write_error_line(str(e)) self.io.write_error_line("") return 1 self.io.write("Done\n") self.io.write(f"Testing {request_title} ... ") try: installer.exists() except PoetryRuntimeError as e: self.io.write("Failed\n") if installer.installation_directory.exists() and self.option("clean"): PythonRemoveCommand.remove_python_installation( str(installer.version), impl, free_threaded, self.io ) raise e self.io.write("Done\n") return 0 ================================================ FILE: src/poetry/console/commands/python/list.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from poetry.core.constraints.version import parse_constraint from poetry.core.version.exceptions import InvalidVersionError from poetry.config.config import Config from poetry.console.commands.command import Command from poetry.utils.env.python import Python if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from poetry.utils.env.python.manager import PythonInfo class PythonListCommand(Command): name = "python list" arguments: ClassVar[list[Argument]] = [ argument("version", "Python version to search for.", optional=True) ] options: ClassVar[list[Option]] = [ option( "all", "a", "List all versions, including those available for download.", flag=True, ), option( "free-threaded", "t", "List only free-threaded Python versions.", flag=True ), option( "implementation", "i", "Python implementation to search for.", flag=False ), option("managed", "m", "List only Poetry managed Python versions.", flag=True), ] description = ( "Shows Python versions available for this environment." " (experimental feature)" ) def handle(self) -> int: rows: list[PythonInfo] = [] constraint = None if self.argument("version"): request = self.argument("version") version = f"~{request}" if request.count(".") < 2 else request try: constraint = parse_constraint(version) except (ValueError, InvalidVersionError): self.io.write_error_line( f"Invalid Python version requested {request}" ) return 1 for info in Python.find_all_versions( constraint=constraint, implementation=self.option("implementation"), free_threaded=self.option("free-threaded") or None, ): rows.append(info) if self.option("all"): for info in Python.find_downloadable_versions(constraint): rows.append(info) rows.sort( key=lambda x: ( x.major, x.minor, x.patch, x.implementation, x.free_threaded, ), reverse=True, ) table = self.table(style="compact") table.set_headers( [ "Version", "Implementation", "Manager", "Path", ] ) implementations = {"cpython": "CPython", "pypy": "PyPy"} python_installation_path = Config.create().python_installation_dir row_count = 0 for pv in rows: version = f"{pv.major}.{pv.minor}.{pv.patch}" if pv.free_threaded: version += "t" implementation = implementations.get( pv.implementation.lower(), pv.implementation ) is_poetry_managed = ( pv.executable is None or pv.executable.resolve().is_relative_to(python_installation_path) ) if self.option("managed") and not is_poetry_managed: continue manager = ( "Poetry" if is_poetry_managed else "System" ) path = ( f"{pv.executable.as_posix()}" if pv.executable else "Available for download" ) table.add_row( [ f"{version}", f"{implementation}", f"{manager}", f"{path}", ] ) row_count += 1 if row_count > 0: table.render() else: self.io.write_line("No Python installations found.") return 0 ================================================ FILE: src/poetry/console/commands/python/remove.py ================================================ from __future__ import annotations import shutil from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from poetry.core.constraints.version.version import Version from poetry.core.version.exceptions import InvalidVersionError from poetry.console.commands.command import Command from poetry.console.commands.python import get_request_title from poetry.utils.env.python.providers import PoetryPythonPathProvider if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from cleo.io.io import IO class PythonRemoveCommand(Command): name = "python remove" arguments: ClassVar[list[Argument]] = [ argument("python", "The python version to remove.", multiple=True) ] options: ClassVar[list[Option]] = [ option( "free-threaded", "t", "Use free-threaded version if available.", flag=True ), option( "implementation", "i", "Python implementation to use. (cpython, pypy)", flag=False, default="cpython", ), ] description = ( "Remove the specified Python version if managed by Poetry." " (experimental feature)" ) @staticmethod def remove_python_installation( request: str, implementation: str, free_threaded: bool, io: IO ) -> int: if request.endswith("t"): free_threaded = True request = request[:-1] try: version = Version.parse(request) except (ValueError, InvalidVersionError): io.write_error_line( f"Invalid Python version requested {request}" ) return 1 if version.minor is None or version.patch is None: io.write_error_line( f"Invalid Python version requested {request}\n" ) io.write_error_line( "You need to provide an exact Python version in the format X.Y.Z to be removed.\n\n" "You can use poetry python list -m to list installed Poetry managed Python versions." ) return 1 request_title = get_request_title(request, implementation, free_threaded) path = PoetryPythonPathProvider.installation_dir( version, implementation, free_threaded ) if path.exists(): if io.is_verbose(): io.write_line(f"Installation path: {path}") io.write(f"Removing installation {request_title} ... ") try: shutil.rmtree(path) except OSError as e: io.write("Failed\n") if io.is_verbose(): io.write_line(f"Failed to remove directory: {e}") io.write("Done\n") else: io.write_line(f"No installation was found at {path}.") return 0 def handle(self) -> int: implementation = self.option("implementation").lower() free_threaded = self.option("free-threaded") result = 0 for request in self.argument("python"): result += self.remove_python_installation( request, implementation, free_threaded, self.io ) return result ================================================ FILE: src/poetry/console/commands/remove.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from tomlkit.toml_document import TOMLDocument from poetry.console.commands.installer_command import InstallerCommand if TYPE_CHECKING: from collections.abc import Mapping from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class RemoveCommand(InstallerCommand): name = "remove" description = "Removes a package from the project dependencies." arguments: ClassVar[list[Argument]] = [ argument("packages", "The packages to remove.", multiple=True) ] options: ClassVar[list[Option]] = [ option("group", "G", "The group to remove the dependency from.", flag=False), option( "dev", "D", "Remove a package from the development dependencies." " (shortcut for '-G dev')", ), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] help = """The remove command removes a package from the current list of installed packages poetry remove""" loggers: ClassVar[list[str]] = [ "poetry.repositories.pypi_repository", "poetry.inspection.info", ] def handle(self) -> int: packages = self.argument("packages") if self.option("dev"): group = "dev" else: group = self.option("group", self.default_group) content: dict[str, Any] = self.poetry.file.read() project_content = content.get("project", {}) groups_content = content.get("dependency-groups", {}) poetry_content = content.get("tool", {}).get("poetry", {}) poetry_groups_content = poetry_content.get("group", {}) if group is None: # remove from all groups removed = set() group_sections = [] project_dependencies = project_content.get("dependencies", []) poetry_dependencies = poetry_content.get("dependencies", {}) if project_dependencies or poetry_dependencies: group_sections.append( (MAIN_GROUP, project_dependencies, poetry_dependencies) ) group_sections.extend( ( group_name, dependencies, poetry_groups_content.get(group_name, {}).get("dependencies", {}), ) for group_name, dependencies in groups_content.items() ) group_sections.extend( (group_name, [], group_section.get("dependencies", {})) for group_name, group_section in poetry_groups_content.items() if group_name not in groups_content and group_name != MAIN_GROUP ) for group_name, standard_section, poetry_section in group_sections: removed |= self._remove_packages( packages=packages, standard_section=standard_section, poetry_section=poetry_section, group_name=group_name, ) if group_name != MAIN_GROUP: if ( not poetry_section and "dependencies" in poetry_groups_content.get(group_name, {}) ): del poetry_content["group"][group_name]["dependencies"] if not poetry_content["group"][group_name]: del poetry_content["group"][group_name] if not standard_section and group_name in groups_content: del groups_content[group_name] if ( group_name not in groups_content and group_name not in poetry_groups_content ): self._remove_references_to_group(group_name, content) elif group == "dev" and "dev-dependencies" in poetry_content: # We need to account for the old `dev-dependencies` section removed = self._remove_packages( packages, [], poetry_content["dev-dependencies"], "dev" ) if not poetry_content["dev-dependencies"]: del poetry_content["dev-dependencies"] else: removed = set() if group_content := poetry_groups_content.get(group): poetry_section = group_content.get("dependencies", {}) removed.update( self._remove_packages( packages=packages, standard_section=[], poetry_section=poetry_section, group_name=group, ) ) if not poetry_section and "dependencies" in group_content: del group_content["dependencies"] if not group_content: del poetry_content["group"][group] if group in groups_content: removed.update( self._remove_packages( packages=packages, standard_section=groups_content[group], poetry_section={}, group_name=group, ) ) if not groups_content[group]: del groups_content[group] if group not in groups_content and group not in poetry_groups_content: self._remove_references_to_group(group, content) if "group" in poetry_content and not poetry_content["group"]: del poetry_content["group"] if "dependency-groups" in content and not content["dependency-groups"]: del content["dependency-groups"] not_found = set(packages).difference(removed) if not_found: raise ValueError( "The following packages were not found: " + ", ".join(sorted(not_found)) ) # Refresh the locker self.poetry.locker.set_pyproject_data(content) self.installer.set_locker(self.poetry.locker) self.installer.set_package(self.poetry.package) self.installer.dry_run(self.option("dry-run", False)) self.installer.verbose(self.io.is_verbose()) self.installer.update(True) self.installer.execute_operations(not self.option("lock")) self.installer.whitelist(removed) status = self.installer.run() if not self.option("dry-run") and status == 0: assert isinstance(content, TOMLDocument) self.poetry.file.write(content) return status def _remove_packages( self, packages: list[str], standard_section: list[str | Mapping[str, Any]], poetry_section: dict[str, Any], group_name: str, ) -> set[str]: removed = set() group = self.poetry.package.dependency_group(group_name) for package in packages: normalized_name = canonicalize_name(package) for requirement in standard_section.copy(): if not isinstance(requirement, str): continue if Dependency.create_from_pep_508(requirement).name == normalized_name: standard_section.remove(requirement) removed.add(package) for existing_package in list(poetry_section): if canonicalize_name(existing_package) == normalized_name: del poetry_section[existing_package] removed.add(package) for package in removed: group.remove_dependency(package) return removed def _remove_references_to_group( self, group_name: str, content: dict[str, Any] ) -> None: """ Removes references to the given group from other groups. """ # 1. PEP 735: [dependency-groups] if "dependency-groups" in content: groups_to_remove = [] for group_key, group_content in content["dependency-groups"].items(): if not isinstance(group_content, list): continue to_remove = [] for item in group_content: if ( isinstance(item, dict) and item.get("include-group") == group_name ): to_remove.append(item) for item in to_remove: group_content.remove(item) # Clean up now-empty lists (normalize with legacy behavior) # Only remove groups that became empty due to include-group cleanup, # not the target group itself (which is handled by the caller) if not group_content and group_key != group_name: groups_to_remove.append(group_key) for group_key in groups_to_remove: del content["dependency-groups"][group_key] # 2. Legacy: [tool.poetry.group.] include-groups = [...] poetry_content = content.get("tool", {}).get("poetry", {}) if "group" in poetry_content: groups_to_remove = [] for group_key, group_content in poetry_content["group"].items(): if "include-groups" not in group_content: continue if group_name in group_content["include-groups"]: group_content["include-groups"].remove(group_name) if not group_content["include-groups"]: del group_content["include-groups"] if not group_content: groups_to_remove.append(group_key) for group_key in groups_to_remove: del poetry_content["group"][group_key] ================================================ FILE: src/poetry/console/commands/run.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from poetry.console.commands.env_command import EnvCommand from poetry.utils._compat import WINDOWS if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from poetry.core.masonry.utils.module import Module class RunCommand(EnvCommand): name = "run" description = "Runs a command in the appropriate environment." arguments: ClassVar[list[Argument]] = [ argument("args", "The command and arguments/options to run.", multiple=True) ] def handle(self) -> int: args = self.argument("args") script = args[0] scripts = self.poetry.local_config.get("scripts") if scripts and script in scripts: return self.run_script(scripts[script], args) try: return self.env.execute(*args) except FileNotFoundError: self.line_error(f"Command not found: {script}") return 1 @property def _module(self) -> Module: from poetry.core.masonry.utils.module import Module poetry = self.poetry package = poetry.package path = poetry.file.path.parent module = Module(package.name, path.as_posix(), package.packages) return module def run_script(self, script: str | dict[str, str], args: list[str]) -> int: """Runs an entry point script defined in the section ``[tool.poetry.scripts]``. When a script exists in the venv bin folder, i.e. after ``poetry install``, then ``sys.argv[0]`` must be set to the full path of the executable, so ``poetry run foo`` and ``poetry shell``, ``foo`` have the same ``sys.argv[0]`` that points to the full path. Otherwise (when an entry point script does not exist), ``sys.argv[0]`` is the script name only, i.e. ``poetry run foo`` has ``sys.argv == ['foo']``. """ for script_dir in self.env.script_dirs: script_path = script_dir / args[0] if WINDOWS: script_path = script_path.with_suffix(".cmd") if script_path.exists(): args = [str(script_path), *args[1:]] break else: # If we reach this point, the script is not installed self._warning_not_installed_script(args[0]) if isinstance(script, dict): script = script["callable"] module, callable_ = script.split(":") src_in_sys_path = "sys.path.append('src'); " if self._module.is_in_src() else "" cmd = ["python", "-c"] cmd += [ "import sys; " "from importlib import import_module; " f"sys.argv = {args!r}; {src_in_sys_path}" f"sys.exit(import_module('{module}').{callable_}())" ] return self.env.execute(*cmd) def _warning_not_installed_script(self, script: str) -> None: message = f"""\ Warning: '{script}' is an entry point defined in pyproject.toml, but it's not \ installed as a script. You may get improper `sys.argv[0]`. The support to run uninstalled scripts will be removed in a future release. Run `poetry install` to resolve and get rid of this message. """ self.line_error(message, style="warning") ================================================ FILE: src/poetry/console/commands/search.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.argument import Argument class SearchCommand(Command): name = "search" description = "Searches for packages on remote repositories." arguments: ClassVar[list[Argument]] = [ argument("tokens", "The tokens to search for.", multiple=True) ] def handle(self) -> int: seen = set() table = self.table(style="compact") table.set_headers( ["Package", "Version", "Source", "Description"] ) rows = [] for repository in self.poetry.pool.repositories: for result in repository.search(self.argument("tokens")): key = f"{repository.name}::{result.pretty_string}" if key in seen: continue seen.add(key) rows.append((result, repository.name)) if not rows: self.line("No matching packages were found.") return 0 for package, source in sorted( rows, key=lambda x: (x[0].name, x[0].version, x[1]) ): table.add_row( [ f"{package.name}", f"{package.version}", f"{source}", str(package.description), ] ) table.render() return 0 ================================================ FILE: src/poetry/console/commands/self/__init__.py ================================================ ================================================ FILE: src/poetry/console/commands/self/add.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from poetry.core.constraints.version import Version from poetry.__version__ import __version__ from poetry.console.commands.add import AddCommand from poetry.console.commands.self.self_command import SelfCommand if TYPE_CHECKING: from cleo.io.inputs.option import Option class SelfAddCommand(SelfCommand, AddCommand): name = "self add" description = "Add additional packages to Poetry's runtime environment." options: ClassVar[list[Option]] = [ o for o in AddCommand.options if o.name in {"editable", "extras", "source", "dry-run", "allow-prereleases"} ] help = f"""\ The self add command installs additional packages to Poetry's runtime \ environment. This is managed in the {SelfCommand.get_default_system_pyproject_file()} \ file. {AddCommand.examples} """ @property def _hint_update_packages(self) -> str: version = Version.parse(__version__) flags = "" if not version.is_stable(): flags = " --preview" return ( "\nIf you want to update it to the latest compatible version, you can use" f" `poetry self update{flags}`.\nIf you prefer to upgrade it to the latest" " available version, you can use `poetry self add package@latest`.\n" ) ================================================ FILE: src/poetry/console/commands/self/install.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from packaging.utils import NormalizedName from packaging.utils import canonicalize_name from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.console.commands.install import InstallCommand from poetry.console.commands.self.self_command import SelfCommand if TYPE_CHECKING: from cleo.io.inputs.option import Option class SelfInstallCommand(SelfCommand, InstallCommand): name = "self install" description = ( "Install locked packages (incl. addons) required by this Poetry installation." ) options: ClassVar[list[Option]] = [ o for o in InstallCommand.options if o.name in {"sync", "dry-run"} ] help = f"""\ The self install command ensures all additional packages specified are \ installed in the current runtime environment. This is managed in the {SelfCommand.get_default_system_pyproject_file()} \ file. You can add more packages using the self add command and remove them using \ the self remove command. """ @property def activated_groups(self) -> set[NormalizedName]: return {MAIN_GROUP, canonicalize_name(self.default_group)} @property def _alternative_sync_command(self) -> str: return "poetry self sync" ================================================ FILE: src/poetry/console/commands/self/lock.py ================================================ from __future__ import annotations from poetry.console.commands.lock import LockCommand from poetry.console.commands.self.self_command import SelfCommand class SelfLockCommand(SelfCommand, LockCommand): name = "self lock" description = "Lock the Poetry installation's system requirements." help = f"""\ The self lock command reads this Poetry installation's system requirements as \ specified in the {SelfCommand.get_default_system_pyproject_file()} file. The system dependencies are locked in the \ {SelfCommand.get_default_system_pyproject_file().parent.joinpath("poetry.lock")} \ file. """ ================================================ FILE: src/poetry/console/commands/self/remove.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from poetry.console.commands.remove import RemoveCommand from poetry.console.commands.self.self_command import SelfCommand if TYPE_CHECKING: from cleo.io.inputs.option import Option class SelfRemoveCommand(SelfCommand, RemoveCommand): name = "self remove" description = "Remove additional packages from Poetry's runtime environment." options: ClassVar[list[Option]] = [ o for o in RemoveCommand.options if o.name in {"dry-run"} ] help = f"""\ The self remove command removes additional package's to Poetry's runtime \ environment. This is managed in the {SelfCommand.get_default_system_pyproject_file()} \ file. """ ================================================ FILE: src/poetry/console/commands/self/self_command.py ================================================ from __future__ import annotations import typing from pathlib import Path from typing import TYPE_CHECKING from typing import Any from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency from poetry.core.packages.project_package import ProjectPackage from poetry.__version__ import __version__ from poetry.console.commands.installer_command import InstallerCommand from poetry.factory import Factory from poetry.pyproject.toml import PyProjectTOML from poetry.utils.constants import POETRY_SYSTEM_PROJECT_NAME from poetry.utils.env import EnvManager from poetry.utils.env import SystemEnv from poetry.utils.helpers import directory if TYPE_CHECKING: from packaging.utils import NormalizedName from poetry.poetry import Poetry from poetry.utils.env import Env class SelfCommand(InstallerCommand): ADDITIONAL_PACKAGE_GROUP = canonicalize_name("additional") @staticmethod def get_default_system_pyproject_file() -> Path: # We separate this out to avoid unwanted side effect during testing while # maintaining dynamic use in help text. # # This is not ideal, but is the simplest solution for now. from poetry.locations import CONFIG_DIR return Path(CONFIG_DIR).joinpath("pyproject.toml") @property def system_pyproject(self) -> Path: file = self.get_default_system_pyproject_file() file.parent.mkdir(parents=True, exist_ok=True) return file def reset_env(self) -> None: self._env = EnvManager.get_system_env(naive=True) @property def env(self) -> Env: if not isinstance(self._env, SystemEnv): self.reset_env() assert self._env is not None return self._env @property def default_group(self) -> str: return self.ADDITIONAL_PACKAGE_GROUP @property def activated_groups(self) -> set[NormalizedName]: return {canonicalize_name(self.default_group)} def generate_system_pyproject(self) -> None: preserved = {} preserved_groups: dict[str, Any] = {} if self.system_pyproject.exists(): toml_file = PyProjectTOML(self.system_pyproject) content = toml_file.data for key in {"group", "source"}: if key in toml_file.poetry_config: preserved[key] = toml_file.poetry_config[key] if "dependency-groups" in content: preserved_groups = typing.cast( "dict[str, Any]", content["dependency-groups"] ) package = ProjectPackage(name=POETRY_SYSTEM_PROJECT_NAME, version=__version__) package.add_dependency(Dependency(name="poetry", constraint=f"{__version__}")) package.python_versions = ".".join(str(v) for v in self.env.version_info[:3]) content = Factory.create_legacy_pyproject_from_package(package=package) content["tool"]["poetry"]["package-mode"] = False # type: ignore[index] for key in preserved: content["tool"]["poetry"][key] = preserved[key] # type: ignore[index] if preserved_groups: content["dependency-groups"] = preserved_groups pyproject = PyProjectTOML(self.system_pyproject) pyproject.file.write(content) def reset_poetry(self) -> None: with directory(self.system_pyproject.parent): self.generate_system_pyproject() self._poetry = Factory().create_poetry( self.system_pyproject.parent, io=self.io, disable_plugins=True ) @property def poetry(self) -> Poetry: if self._poetry is None: self.reset_poetry() assert self._poetry is not None return self._poetry def _system_project_handle(self) -> int: """ This is a helper method that by default calls the handle method implemented in the child class's next MRO sibling. Override this if you want special handling either before calling the handle() from the super class or have custom logic to handle the command. The default implementations handles cases where a `self` command delegates handling to an existing command. Eg: `SelfAddCommand(SelfCommand, AddCommand)`. """ return_code: int = super().handle() return return_code def reset(self) -> None: """ Reset current command instance's environment and poetry instances to ensure use of the system specific ones. """ self.reset_env() self.reset_poetry() def handle(self) -> int: # We override the base class's handle() method to ensure that poetry and env # are reset to work within the system project instead of current context. # Further, during execution, the working directory is temporarily changed # to parent directory of Poetry system pyproject.toml file. # # This method **should not** be overridden in child classes as it may have # unexpected consequences. self.reset() with directory(self.system_pyproject.parent): return self._system_project_handle() ================================================ FILE: src/poetry/console/commands/self/show/__init__.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import option from poetry.console.commands.self.self_command import SelfCommand from poetry.console.commands.show import ShowCommand if TYPE_CHECKING: from cleo.io.inputs.option import Option from packaging.utils import NormalizedName class SelfShowCommand(SelfCommand, ShowCommand): name = "self show" options: ClassVar[list[Option]] = [ option("addons", None, "List only add-on packages installed."), *[ o for o in ShowCommand.options if o.name in {"tree", "latest", "outdated", "format"} ], ] description = "Show packages from Poetry's runtime environment." help = f"""\ The self show command behaves similar to the show command, but working within Poetry's runtime environment. This lists all packages installed within the Poetry install environment. To show only additional packages that have been added via self add and their dependencies use self show --addons. This is managed in the {SelfCommand.get_default_system_pyproject_file()} \ file. """ @property def activated_groups(self) -> set[NormalizedName]: if self.option("addons", False): return {SelfCommand.ADDITIONAL_PACKAGE_GROUP} return super(ShowCommand, self).activated_groups ================================================ FILE: src/poetry/console/commands/self/show/plugins.py ================================================ from __future__ import annotations import dataclasses from typing import TYPE_CHECKING from poetry.console.commands.self.self_command import SelfCommand if TYPE_CHECKING: from importlib import metadata from poetry.core.packages.package import Package @dataclasses.dataclass class PluginPackage: package: Package plugins: list[metadata.EntryPoint] = dataclasses.field(default_factory=list) application_plugins: list[metadata.EntryPoint] = dataclasses.field( default_factory=list ) def append(self, entry_point: metadata.EntryPoint) -> None: from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin import Plugin group = entry_point.group if group == ApplicationPlugin.group: self.application_plugins.append(entry_point) elif group == Plugin.group: self.plugins.append(entry_point) else: name = entry_point.name raise ValueError(f"Unknown plugin group ({group}) for {name}") class SelfShowPluginsCommand(SelfCommand): name = "self show plugins" description = "Shows information about the currently installed plugins." help = """\ The self show plugins command lists all installed Poetry plugins. Plugins can be added and removed using the self add and self remove \ commands respectively. This command does not list packages that do not provide a Poetry plugin. """ def _system_project_handle(self) -> int: from packaging.utils import canonicalize_name from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin import Plugin from poetry.plugins.plugin_manager import PluginManager from poetry.repositories.installed_repository import InstalledRepository from poetry.utils.env import EnvManager from poetry.utils.helpers import pluralize plugins: dict[str, PluginPackage] = {} system_env = EnvManager.get_system_env(naive=True) installed_repository = InstalledRepository.load( system_env, with_dependencies=True ) packages_by_name: dict[str, Package] = { pkg.name: pkg for pkg in installed_repository.packages } for group in [ApplicationPlugin.group, Plugin.group]: for entry_point in PluginManager(group).get_plugin_entry_points(): assert entry_point.dist is not None package = packages_by_name[canonicalize_name(entry_point.dist.name)] name = package.pretty_name info = plugins.get(name) or PluginPackage(package=package) info.append(entry_point) plugins[name] = info for name, info in plugins.items(): package = info.package description = " " + package.description if package.description else "" self.line("") self.line(f" - {name} ({package.version}){description}") provide_line = " " if info.plugins: count = len(info.plugins) provide_line += f" {count} plugin{pluralize(count)}" if info.application_plugins: if info.plugins: provide_line += " and" count = len(info.application_plugins) provide_line += ( f" {count} application plugin{pluralize(count)}" ) self.line(provide_line) if package.requires: self.line("") self.line(" Dependencies") for dependency in package.requires: self.line( f" - {dependency.pretty_name}" f" ({dependency.pretty_constraint})" ) return 0 ================================================ FILE: src/poetry/console/commands/self/sync.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from poetry.console.commands.self.install import SelfInstallCommand if TYPE_CHECKING: from cleo.io.inputs.option import Option class SelfSyncCommand(SelfInstallCommand): name = "self sync" description = ( "Sync Poetry's own environment according to the locked packages (incl. addons)" " required by this Poetry installation." ) options: ClassVar[list[Option]] = [ opt for opt in SelfInstallCommand.options if opt.name != "sync" ] help = f"""\ The self sync command ensures all additional (and no other) packages \ specified are installed in the current runtime environment. This is managed in the \ {SelfInstallCommand.get_default_system_pyproject_file()} file. You can add more packages using the self add command and remove them using \ the self remove command. """ @property def _with_synchronization(self) -> bool: return True ================================================ FILE: src/poetry/console/commands/self/update.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from cleo.io.inputs.string_input import StringInput from cleo.io.io import IO from poetry.console.commands.add import AddCommand from poetry.console.commands.self.self_command import SelfCommand if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class SelfUpdateCommand(SelfCommand): name = "self update" description = "Updates Poetry to the latest version." arguments: ClassVar[list[Argument]] = [ argument( "version", "The version to update to.", optional=True, default="latest" ) ] options: ClassVar[list[Option]] = [ option("preview", None, "Allow the installation of pre-release versions."), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), ] help = """\ The self update command updates Poetry version in its current runtime \ environment. """ def _system_project_handle(self) -> int: self.write("Updating Poetry version ...\n\n") application = self.get_application() add_command = application.find("add") assert isinstance(add_command, AddCommand) add_command.set_env(self.env) add_command.set_poetry(self.poetry) application.configure_installer_for_command(add_command, self.io) argv = ["add", f"poetry@{self.argument('version')}"] if self.option("dry-run"): argv.append("--dry-run") if self.option("preview"): argv.append("--allow-prereleases") exit_code: int = add_command.run( IO( StringInput(" ".join(argv)), self.io.output, self.io.error_output, ) ) return exit_code ================================================ FILE: src/poetry/console/commands/show.py ================================================ from __future__ import annotations import json import sys from enum import Enum from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name from poetry.console.commands.env_command import EnvCommand from poetry.console.commands.group_command import GroupCommand from poetry.utils.constants import POETRY_SYSTEM_PROJECT_NAME if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from cleo.io.io import IO from cleo.ui.table import Rows from packaging.utils import NormalizedName from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.repositories.repository import Repository def reverse_deps(pkg: Package, repo: Repository) -> dict[str, str]: required_by = {} for locked in repo.packages: dependencies = {d.name: d.pretty_constraint for d in locked.requires} if pkg.name in dependencies: required_by[locked.pretty_name] = dependencies[pkg.name] return required_by class OutputFormats(str, Enum): JSON = "json" TEXT = "text" class ShowCommand(GroupCommand, EnvCommand): name = "show" description = "Shows information about packages." arguments: ClassVar[list[Argument]] = [ argument("package", "The package to inspect", optional=True) ] options: ClassVar[list[Option]] = [ *GroupCommand._group_dependency_options(), option("tree", "t", "List the dependencies as a tree."), option( "why", None, "When showing the full list, or a --tree for a single package," " display whether they are a direct dependency or required by other" " packages", ), option("latest", "l", "Show the latest version."), option( "outdated", "o", "Show the latest version but only for packages that are outdated.", ), option( "all", "a", "Show all packages (even those not compatible with current system).", ), option("top-level", "T", "Show only top-level dependencies."), option( "no-truncate", None, "Do not truncate the output based on the terminal width.", ), option( "format", "f", "Specify the output format (`json` or `text`). Default is `text`. `json` cannot be combined with the --tree option.", flag=False, default="text", ), ] help = """The show command displays detailed information about a package, or lists all packages available.""" colors: ClassVar[list[str]] = ["cyan", "yellow", "green", "magenta", "blue"] def handle(self) -> int: package = self.argument("package") if self.option("tree"): self.init_styles(self.io) if self.option("top-level"): if self.option("tree"): self.line_error( "Error: Cannot use --tree and --top-level at the same" " time." ) return 1 if package is not None: self.line_error( "Error: Cannot use --top-level when displaying a single" " package." ) return 1 if self.option("why"): if self.option("tree") and package is None: self.line_error( "Error: --why requires a package when combined with" " --tree." ) return 1 if not self.option("tree") and package: self.line_error( "Error: --why cannot be used without --tree when displaying" " a single package." ) return 1 if self.option("format") not in set(OutputFormats): self.line_error( f"Error: Invalid output format. Supported formats are: {', '.join(OutputFormats)}." ) return 1 if self.option("format") != OutputFormats.TEXT and self.option("tree"): self.line_error( "Error: --tree option can only be used with the text output option." ) return 1 if self.option("outdated"): self.io.input.set_option("latest", True) if not self.poetry.locker.is_locked(): self.line_error( f"Error: poetry.lock not found. Run `{self._lock_create_command()}`" " to create it." ) return 1 locked_repo = self.poetry.locker.locked_repository() if package: return self._display_single_package_information(package, locked_repo) root = self.project_with_activated_groups_only() # Show tree view if requested if self.option("tree"): return self._display_packages_tree_information(locked_repo, root) return self._display_packages_information(locked_repo, root) def _lock_create_command(self) -> str: if self.poetry.package.name == POETRY_SYSTEM_PROJECT_NAME: return "poetry self lock" return "poetry lock" def _display_single_package_information( self, package: str, locked_repository: Repository ) -> int: locked_packages = locked_repository.packages canonicalized_package = canonicalize_name(package) pkg = None for locked in locked_packages: if locked.name == canonicalized_package: pkg = locked break if not pkg: raise ValueError(f"Package {package} not found") required_by = reverse_deps(pkg, locked_repository) if self.option("tree"): if self.option("why"): # The default case if there's no reverse dependencies is to query # the subtree for pkg but if any rev-deps exist we'll query for each # of them in turn packages = [pkg] if required_by: packages = [ p for p in locked_packages for r in required_by if p.name == r ] else: # if no rev-deps exist we'll make this clear as it can otherwise # look very odd for packages that also have no or few direct # dependencies self.io.write_line(f"Package {package} is a direct dependency.") for p in packages: self.display_package_tree( self.io, p, locked_packages, why_package=pkg ) else: self.display_package_tree(self.io, pkg, locked_packages) return 0 if self.option("format") == OutputFormats.JSON: package_info: dict[str, str | dict[str, str]] = { "name": pkg.pretty_name, "version": pkg.pretty_version, "description": pkg.description, } if pkg.requires: package_info["dependencies"] = { dependency.pretty_name: dependency.pretty_constraint for dependency in pkg.requires } if required_by: package_info["required_by"] = required_by self.line(json.dumps(package_info)) return 0 rows: Rows = [ ["name", f" : {pkg.pretty_name}"], ["version", f" : {pkg.pretty_version}"], ["description", f" : {pkg.description}"], ] self.table(rows=rows, style="compact").render() if pkg.requires: self.line("") self.line("dependencies") for dependency in pkg.requires: self.line( f" - {dependency.pretty_name}" f" {dependency.pretty_constraint}" ) if required_by: self.line("") self.line("required by") for parent, requires_version in required_by.items(): self.line(f" - {parent} requires {requires_version}") return 0 def _display_packages_information( self, locked_repository: Repository, root: ProjectPackage ) -> int: import shutil from cleo.io.null_io import NullIO from poetry.puzzle.solver import Solver from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.helpers import get_package_version_display_string locked_packages = locked_repository.packages pool = RepositoryPool.from_packages(locked_packages, self.poetry.config) solver = Solver( root, pool=pool, installed=[], locked=locked_packages, io=NullIO(), ) solver.provider.load_deferred(False) with solver.use_environment(self.env): ops = solver.solve().calculate_operations() required_locked_packages = {op.package for op in ops if not op.skipped} show_latest = self.option("latest") show_all = self.option("all") show_top_level = self.option("top-level") show_why = self.option("why") width = ( sys.maxsize if self.option("no-truncate") else shutil.get_terminal_size().columns ) name_length = version_length = latest_length = required_by_length = 0 latest_packages = {} latest_statuses = {} installed_repo = InstalledRepository.load(self.env) requires = root.all_requires # Computing widths for locked in locked_packages: if locked not in required_locked_packages and not show_all: continue current_length = len(locked.pretty_name) if not self.io.output.is_decorated(): installed_status = self.get_installed_status( locked, installed_repo.packages ) if installed_status == "not-installed": current_length += 4 if show_latest: latest = self.find_latest_package(locked, root) if not latest: latest = locked latest_packages[locked.pretty_name] = latest update_status = latest_statuses[locked.pretty_name] = ( self.get_update_status(latest, locked) ) if not self.option("outdated") or update_status != "up-to-date": name_length = max(name_length, current_length) version_length = max( version_length, len( get_package_version_display_string( locked, root=self.poetry.file.path.parent ) ), ) latest_length = max( latest_length, len( get_package_version_display_string( latest, root=self.poetry.file.path.parent ) ), ) if show_why: required_by = reverse_deps(locked, locked_repository) required_by_length = max( required_by_length, len(" from " + ",".join(required_by.keys())), ) else: name_length = max(name_length, current_length) version_length = max( version_length, len( get_package_version_display_string( locked, root=self.poetry.file.path.parent ) ), ) if show_why: required_by = reverse_deps(locked, locked_repository) required_by_length = max( required_by_length, len(" from " + ",".join(required_by.keys())) ) if self.option("format") == OutputFormats.JSON: packages = [] for locked in locked_packages: if locked not in required_locked_packages and not show_all: continue if ( show_latest and self.option("outdated") and latest_statuses[locked.pretty_name] == "up-to-date" ): continue if show_top_level and not any(locked.satisfies(r) for r in requires): continue package: dict[str, str | list[str]] = {} package["name"] = locked.pretty_name package["installed_status"] = self.get_installed_status( locked, installed_repo.packages ) package["version"] = get_package_version_display_string( locked, root=self.poetry.file.path.parent ) if show_latest: latest = latest_packages[locked.pretty_name] package["latest_version"] = get_package_version_display_string( latest, root=self.poetry.file.path.parent ) if show_why: required_by = reverse_deps(locked, locked_repository) if required_by: package["required_by"] = list(required_by.keys()) package["description"] = locked.description packages.append(package) self.line(json.dumps(packages)) return 0 write_version = name_length + version_length + 3 <= width write_latest = name_length + version_length + latest_length + 3 <= width why_end_column = ( name_length + version_length + latest_length + required_by_length ) write_why = show_why and (why_end_column + 3) <= width write_description = (why_end_column + 24) <= width for locked in locked_packages: color = "cyan" name = locked.pretty_name install_marker = "" if show_top_level and not any(locked.satisfies(r) for r in requires): continue if locked not in required_locked_packages: if not show_all: continue color = "black;options=bold" else: installed_status = self.get_installed_status( locked, installed_repo.packages ) if installed_status == "not-installed": color = "red" if not self.io.output.is_decorated(): # Non installed in non decorated mode install_marker = " (!)" if ( show_latest and self.option("outdated") and latest_statuses[locked.pretty_name] == "up-to-date" ): continue line = ( f"" f"{name:{name_length - len(install_marker)}}{install_marker}" ) if write_version: version = get_package_version_display_string( locked, root=self.poetry.file.path.parent ) line += f" {version:{version_length}}" if show_latest: latest = latest_packages[locked.pretty_name] update_status = latest_statuses[locked.pretty_name] if write_latest: color = "green" if update_status == "semver-safe-update": color = "red" elif update_status == "update-possible": color = "yellow" version = get_package_version_display_string( latest, root=self.poetry.file.path.parent ) line += f" {version:{latest_length}}" if write_why: required_by = reverse_deps(locked, locked_repository) if required_by: content = ",".join(required_by.keys()) # subtract 6 for ' from ' line += f" from {content:{required_by_length - 6}}" else: line += " " * required_by_length if write_description: description = locked.description remaining = ( width - name_length - version_length - required_by_length - 4 ) if show_latest: remaining -= latest_length if len(locked.description) > remaining: description = description[: remaining - 3] + "..." line += " " + description self.line(line) return 0 def _display_packages_tree_information( self, locked_repository: Repository, root: ProjectPackage ) -> int: packages = locked_repository.packages for p in packages: for require in root.all_requires: if p.name == require.name: self.display_package_tree(self.io, p, packages) break return 0 def display_package_tree( self, io: IO, package: Package, installed_packages: list[Package], why_package: Package | None = None, ) -> None: io.write(f"{package.pretty_name}") description = "" if package.description: description = " " + package.description io.write_line(f" {package.pretty_version}{description}") if why_package is not None: dependencies = [p for p in package.requires if p.name == why_package.name] else: dependencies = package.requires dependencies = sorted( dependencies, key=lambda x: x.name, ) tree_bar = "├" total = len(dependencies) for i, dependency in enumerate(dependencies, 1): if i == total: tree_bar = "└" level = 1 color = self.colors[level] info = ( f"{tree_bar}── <{color}>{dependency.name}" f" {dependency.pretty_constraint}" ) self._write_tree_line(io, info) tree_bar = tree_bar.replace("└", " ") packages_in_tree = [package.name, dependency.name] self._display_tree( io, dependency, installed_packages, packages_in_tree, tree_bar, level + 1, ) def _display_tree( self, io: IO, dependency: Dependency, installed_packages: list[Package], packages_in_tree: list[NormalizedName], previous_tree_bar: str = "├", level: int = 1, ) -> None: previous_tree_bar = previous_tree_bar.replace("├", "│") dependencies = [] for package in installed_packages: if package.name == dependency.name: dependencies = package.requires break dependencies = sorted( dependencies, key=lambda x: x.name, ) tree_bar = previous_tree_bar + " ├" total = len(dependencies) for i, dependency in enumerate(dependencies, 1): current_tree = packages_in_tree if i == total: tree_bar = previous_tree_bar + " └" color_ident = level % len(self.colors) color = self.colors[color_ident] circular_warn = "" if dependency.name in current_tree: circular_warn = "(circular dependency aborted here)" info = ( f"{tree_bar}── <{color}>{dependency.name}" f" {dependency.pretty_constraint} {circular_warn}" ) self._write_tree_line(io, info) tree_bar = tree_bar.replace("└", " ") if dependency.name not in current_tree: current_tree.append(dependency.name) self._display_tree( io, dependency, installed_packages, current_tree, tree_bar, level + 1, ) def _write_tree_line(self, io: IO, line: str) -> None: if not io.output.supports_utf8(): line = line.replace("└", "`-") line = line.replace("├", "|-") line = line.replace("──", "-") line = line.replace("│", "|") io.write_line(line) def init_styles(self, io: IO) -> None: from cleo.formatters.style import Style for color in self.colors: style = Style(color) io.output.formatter.set_style(color, style) io.error_output.formatter.set_style(color, style) def find_latest_package( self, package: Package, root: ProjectPackage ) -> Package | None: from cleo.io.null_io import NullIO from poetry.puzzle.provider import Provider from poetry.version.version_selector import VersionSelector # find the latest version allowed in this pool requires = root.all_requires if package.is_direct_origin(): for dep in requires: if dep.name == package.name and dep.source_type == package.source_type: provider = Provider(root, self.poetry.pool, NullIO()) return provider.search_for_direct_origin_dependency(dep) allow_prereleases: bool | None = None for dep in requires: if dep.name == package.name: allow_prereleases = dep.allows_prereleases() break name = package.name selector = VersionSelector(self.poetry.pool) return selector.find_best_candidate( name, f">={package.pretty_version}", allow_prereleases ) def get_update_status(self, latest: Package, package: Package) -> str: from poetry.core.constraints.version import parse_constraint if latest.full_pretty_version == package.full_pretty_version: return "up-to-date" constraint = parse_constraint("^" + package.pretty_version) if constraint.allows(latest.version): # It needs an immediate semver-compliant upgrade return "semver-safe-update" # it needs an upgrade but has potential BC breaks so is not urgent return "update-possible" def get_installed_status( self, locked: Package, installed_packages: list[Package] ) -> str: for package in installed_packages: if locked.name == package.name: return "installed" return "not-installed" ================================================ FILE: src/poetry/console/commands/source/__init__.py ================================================ ================================================ FILE: src/poetry/console/commands/source/add.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from cleo.io.null_io import NullIO from tomlkit import table from tomlkit.items import AoT from poetry.config.source import Source from poetry.console.commands.command import Command from poetry.repositories.repository_pool import Priority if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class SourceAddCommand(Command): name = "source add" description = "Add source configuration for project." arguments: ClassVar[list[Argument]] = [ argument( "name", "Source repository name.", ), argument( "url", "Source repository URL." " Required, except for PyPI, for which it is not allowed.", optional=True, ), ] options: ClassVar[list[Option]] = [ option( "priority", "p", "Set the priority of this source. One of:" f" {', '.join(p.name.lower() for p in Priority)}. Defaults to" f" {Priority.PRIMARY.name.lower()}, but will switch to " f"{Priority.SUPPLEMENTAL.name.lower()} in a later release.", flag=False, ), ] def handle(self) -> int: from poetry.factory import Factory name: str = self.argument("name") lower_name = name.lower() url: str = self.argument("url") priority_str: str | None = self.option("priority", None) if lower_name == "pypi": name = "PyPI" if url: self.line_error( "The URL of PyPI is fixed and cannot be set." ) return 1 elif not url: self.line_error( "A custom source cannot be added without a URL." ) return 1 if priority_str is None: self.io.write_error_line( f"The default priority will change to {Priority.SUPPLEMENTAL.name.lower()} " f"in a future release." ) priority = Priority.PRIMARY else: priority = Priority[priority_str.upper()] sources = AoT([]) new_source = Source(name=name, url=url, priority=priority) is_new_source = True for source in self.poetry.get_sources(): if source.name.lower() == lower_name: source = new_source is_new_source = False sources.append(source.to_toml_table()) if is_new_source: self.line(f"Adding source with name {name}.") sources.append(new_source.to_toml_table()) else: self.line(f"Source with name {name} already exists. Updating.") # ensure new source is valid. eg: invalid name etc. try: pool = Factory.create_pool(self.poetry.config, sources, NullIO()) pool.repository(name) except ValueError as e: self.line_error( f"Failed to validate addition of {name}: {e}" ) return 1 # tomlkit types are awkward to work with, treat content as a mostly untyped # dictionary. content: dict[str, Any] = self.poetry.pyproject.data if "tool" not in content: content["tool"] = table() if "poetry" not in content["tool"]: content["tool"]["poetry"] = table() self.poetry.pyproject.poetry_config["source"] = sources self.poetry.pyproject.save() return 0 ================================================ FILE: src/poetry/console/commands/source/remove.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from tomlkit.items import AoT from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.argument import Argument class SourceRemoveCommand(Command): name = "source remove" description = "Remove source configured for the project." arguments: ClassVar[list[Argument]] = [ argument( "name", "Source repository name.", ), ] def handle(self) -> int: name = self.argument("name") lower_name = name.lower() sources = AoT([]) removed = False for source in self.poetry.get_sources(): if source.name.lower() == lower_name: self.line(f"Removing source with name {source.name}.") removed = True continue sources.append(source.to_toml_table()) if not removed: self.line_error( f"Source with name {name} was not found." ) return 1 self.poetry.pyproject.poetry_config["source"] = sources self.poetry.pyproject.save() return 0 ================================================ FILE: src/poetry/console/commands/source/show.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.ui.table import Rows class SourceShowCommand(Command): name = "source show" description = "Show information about sources configured for the project." arguments: ClassVar[list[Argument]] = [ argument( "source", "Source(s) to show information for. Defaults to showing all sources.", optional=True, multiple=True, ), ] def notify_implicit_pypi(self) -> None: if not self.poetry.pool.has_repository("pypi"): return self.line( "PyPI is implicitly enabled as a primary source. " "If you wish to disable it, or alter its priority please refer to " "https://python-poetry.org/docs/repositories/#package-sources." ) self.line("") def handle(self) -> int: sources = self.poetry.get_sources() names = self.argument("source") lower_names = [name.lower() for name in names] if not sources: self.line("No sources configured for this project.\n") self.notify_implicit_pypi() return 0 if names and not any(s.name.lower() in lower_names for s in sources): self.line_error( f"No source found with name(s): {', '.join(names)}", style="error", ) return 1 is_pypi_implicit = True for source in sources: if names and source.name.lower() not in lower_names: continue if source.name.lower() == "pypi": is_pypi_implicit = False table = self.table(style="compact") rows: Rows = [["name", f" : {source.name}"]] if source.url: rows.append(["url", f" : {source.url}"]) rows.append(["priority", f" : {source.priority.name.lower()}"]) table.add_rows(rows) table.render() self.line("") if not names and is_pypi_implicit: self.notify_implicit_pypi() return 0 ================================================ FILE: src/poetry/console/commands/sync.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from poetry.console.commands.install import InstallCommand if TYPE_CHECKING: from cleo.io.inputs.option import Option class SyncCommand(InstallCommand): name = "sync" description = "Update the project's environment according to the lockfile." options: ClassVar[list[Option]] = [ opt for opt in InstallCommand.options if opt.name != "sync" ] help = """\ The sync command makes sure that the project's environment is in sync with the poetry.lock file. It is equivalent to running poetry install --sync. poetry sync By default, the above command will also install the current project. To install only the dependencies and not including the current project, run the command with the --no-root option like below: poetry sync --no-root If you want to use Poetry only for dependency management but not for packaging, you can set the "package-mode" to false in your pyproject.toml file. """ @property def _with_synchronization(self) -> bool: return True ================================================ FILE: src/poetry/console/commands/update.py ================================================ from __future__ import annotations import re from typing import TYPE_CHECKING from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name from poetry.console.commands.installer_command import InstallerCommand if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option _VERSION_SPECIFIER_RE = re.compile(r"[><=!~]") class UpdateCommand(InstallerCommand): name = "update" description = ( "Update the dependencies as according to the pyproject.toml file." ) arguments: ClassVar[list[Argument]] = [ argument("packages", "The packages to update", optional=True, multiple=True) ] options: ClassVar[list[Option]] = [ *InstallerCommand._group_dependency_options(), option( "sync", None, "Synchronize the environment with the locked packages and the specified" " groups.", ), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] loggers: ClassVar[list[str]] = ["poetry.repositories.pypi_repository"] def handle(self) -> int: packages = self.argument("packages") if packages: # Detect version specifiers in package arguments — poetry update # only accepts bare package names, not requirement strings. packages_with_specifiers = [ p for p in packages if _VERSION_SPECIFIER_RE.search(p) ] if packages_with_specifiers: self.line_error( "Version specifiers are not allowed in" " poetry update." ) for pkg in packages_with_specifiers: self.line_error(f" - {pkg}") self.line_error( "Use poetry add to change version constraints." ) return 1 # Validate that all specified packages are declared dependencies all_dependencies = {dep.name for dep in self.poetry.package.all_requires} invalid_packages = [ p for p in packages if canonicalize_name(p) not in all_dependencies ] if invalid_packages: self.line_error( "The following packages are not dependencies" f" of this project: {', '.join(invalid_packages)}" ) return 1 self.installer.whitelist(dict.fromkeys(packages, "*")) self.installer.only_groups(self.activated_groups) self.installer.dry_run(self.option("dry-run")) self.installer.requires_synchronization(self.option("sync")) self.installer.execute_operations(not self.option("lock")) # Force update self.installer.update(True) return self.installer.run() ================================================ FILE: src/poetry/console/commands/version.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from cleo.helpers import argument from cleo.helpers import option from poetry.core.version.exceptions import InvalidVersionError from tomlkit.toml_document import TOMLDocument from poetry.console.commands.command import Command if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from poetry.core.constraints.version import Version class VersionCommand(Command): name = "version" description = ( "Shows the version of the project or bumps it when a valid " "bump rule is provided." ) arguments: ClassVar[list[Argument]] = [ argument( "version", "The version number or the rule to update the version.", optional=True, ), ] options: ClassVar[list[Option]] = [ option("short", "s", "Output the version number only"), option( "dry-run", None, "Do not update pyproject.toml file", ), option("next-phase", None, "Increment the phase of the current version"), ] help = """\ The version command shows the current version of the project or bumps the version of the project and writes the new version back to pyproject.toml if a valid bump rule is provided. The new version should ideally be a valid semver string or a valid bump rule: patch, minor, major, prepatch, preminor, premajor, prerelease. """ def handle(self) -> int: version = self.argument("version") if version: version = self.increment_version( self.poetry.package.pretty_version, version, self.option("next-phase") ) if self.option("short"): self.line(version.to_string()) else: self.line( f"Bumping version from {self.poetry.package.pretty_version}" f" to {version}" ) if not self.option("dry-run"): content: dict[str, Any] = self.poetry.file.read() project_content = content.get("project", {}) if "version" in project_content: project_content["version"] = version.text poetry_content = content.get("tool", {}).get("poetry", {}) if "version" in poetry_content: poetry_content["version"] = version.text assert isinstance(content, TOMLDocument) self.poetry.file.write(content) else: if self.option("short"): self.line(self.poetry.package.pretty_version) else: self.line( f"{self.poetry.package.pretty_name}" f" {self.poetry.package.pretty_version}" ) return 0 def increment_version( self, version: str, rule: str, next_phase: bool = False ) -> Version: from poetry.core.constraints.version import Version try: parsed = Version.parse(version) except InvalidVersionError: raise ValueError("The project's version doesn't seem to follow semver") if rule in {"major", "premajor"}: new = parsed.next_major() if rule == "premajor": new = new.first_prerelease() elif rule in {"minor", "preminor"}: new = parsed.next_minor() if rule == "preminor": new = new.first_prerelease() elif rule in {"patch", "prepatch"}: new = parsed.next_patch() if rule == "prepatch": new = new.first_prerelease() elif rule == "prerelease": if parsed.is_unstable(): pre = parsed.pre assert pre is not None pre = pre.next_phase() if next_phase else pre.next() new = Version(parsed.epoch, parsed.release, pre) else: new = parsed.next_patch().first_prerelease() else: new = Version.parse(rule) return new ================================================ FILE: src/poetry/console/events/__init__.py ================================================ ================================================ FILE: src/poetry/console/events/console_events.py ================================================ ================================================ FILE: src/poetry/console/exceptions.py ================================================ from __future__ import annotations import dataclasses import shlex from dataclasses import InitVar from subprocess import CalledProcessError from typing import TYPE_CHECKING from cleo.exceptions import CleoError from poetry.utils._compat import decode if TYPE_CHECKING: from cleo.io.io import IO class PoetryConsoleError(CleoError): pass class GroupNotFoundError(PoetryConsoleError): pass @dataclasses.dataclass class ConsoleMessage: """ Representation of a console message, providing utilities for formatting text with tags, indentation, and sections. The ConsoleMessage class is designed to represent text messages that might be displayed in a console or terminal output. It provides features for managing formatted text, such as stripping tags, wrapping text with specific tags, indenting, and creating structured message sections. """ text: str debug: bool = False @property def stripped(self) -> str: from cleo._utils import strip_tags return strip_tags(self.text) def wrap(self, tag: str) -> ConsoleMessage: if self.text: self.text = f"<{tag}>{self.text}" return self def indent(self, indent: str) -> ConsoleMessage: if self.text: self.text = f"\n{indent}".join(self.text.splitlines()).strip() self.text = f"{indent}{self.text}" return self def make_section( self, title: str, indent: str = "", ) -> ConsoleMessage: if not self.text: return self if self.text: section = [f"{title}:"] if title else [] section.extend(self.text.splitlines()) self.text = f"\n{indent}".join(section).strip() return self @dataclasses.dataclass class PrettyCalledProcessError: """ Represents a formatted and decorated error object for a subprocess call. This class is used to encapsulate information about a `CalledProcessError`, providing additional context such as command output, errors, and helpful debugging messages. It is particularly useful for wrapping and decorating subprocess-related exceptions in a more user-friendly format. Attributes: message: A string representation of the exception. output: A section formatted representation of the exception stdout. errors: A section formatted representation of the exception stderr. command_message: Formatted message including a hint on retrying the original command. command: A `shelex` quoted string representation of the original command. exception: The original `CalledProcessError` instance. indent: Indent prefix to use for inner content per section. """ message: ConsoleMessage = dataclasses.field(init=False) output: ConsoleMessage = dataclasses.field(init=False) errors: ConsoleMessage = dataclasses.field(init=False) command_message: ConsoleMessage = dataclasses.field(init=False) command: str = dataclasses.field(init=False) exception: InitVar[CalledProcessError] = dataclasses.field(init=True) indent: InitVar[str] = dataclasses.field(default="") def __post_init__(self, exception: CalledProcessError, indent: str) -> None: self.message = ConsoleMessage(str(exception).strip(), debug=True).make_section( "Exception", indent ) self.output = ConsoleMessage(decode(exception.stdout), debug=True).make_section( "Output", indent ) self.errors = ConsoleMessage(decode(exception.stderr), debug=True).make_section( "Errors", indent ) self.command = ( shlex.join(exception.cmd) if isinstance(exception.cmd, list) else exception.cmd ) self.command_message = ConsoleMessage( f"You can test the failed command by executing:\n\n {self.command}", debug=False, ) class PoetryRuntimeError(PoetryConsoleError): """ Represents a runtime error in the Poetry console application. """ def __init__( self, reason: str, messages: list[ConsoleMessage] | None = None, exit_code: int = 1, ) -> None: super().__init__(reason) self.exit_code = exit_code self._messages = messages or [] self._messages.insert(0, ConsoleMessage(reason)) def write(self, io: IO) -> None: """ Write the error text to the provided IO iff there is any text to write. """ if text := self.get_text(debug=io.is_verbose(), strip=False): io.write_error_line(text) def get_text( self, debug: bool = False, indent: str = "", strip: bool = False ) -> str: """ Convert the error messages to a formatted string. All empty messages are ignored along with debug level messages if `debug` is `False`. """ text = "" has_skipped_debug = False for message in self._messages: if message.debug and not debug: has_skipped_debug = True continue message_text = message.stripped if strip else message.text if not message_text: continue if indent: message_text = f"\n{indent}".join(message_text.splitlines()) text += f"{indent}{message_text}\n{indent}\n" if has_skipped_debug: message = ConsoleMessage( f"{indent}You can also run your poetry command with -v to see more information.\n{indent}\n" ) text += message.stripped if strip else message.text return text.rstrip(f"{indent}\n") def __str__(self) -> str: return self._messages[0].stripped.strip() @classmethod def create( cls, reason: str, exception: CalledProcessError | Exception | None = None, info: list[str] | str | None = None, ) -> PoetryRuntimeError: """ Create an instance of this class using the provided reason. If an exception is provided, this is also injected as a debug `ConsoleMessage`. There is specific handling for known exception types. For example, if exception is of type `subprocess.CalledProcessError`, the following sections are additionally added when available - stdout, stderr and command for testing. """ if isinstance(info, str): info = [info] messages: list[ConsoleMessage] = [ ConsoleMessage( "\n".join(info or []), debug=False, ).wrap("info"), ] if isinstance(exception, CalledProcessError): error = PrettyCalledProcessError(exception, indent=" | ") messages = [ error.message.wrap("warning"), error.output.wrap("warning"), error.errors.wrap("warning"), *messages, error.command_message, ] elif exception is not None and isinstance(exception, Exception): messages.insert( 0, ConsoleMessage(str(exception), debug=True).make_section( "Exception", indent=" | " ), ) return cls(reason, messages) def append(self, message: str | ConsoleMessage) -> PoetryRuntimeError: if isinstance(message, str): message = ConsoleMessage(message) self._messages.append(message) return self ================================================ FILE: src/poetry/console/logging/__init__.py ================================================ ================================================ FILE: src/poetry/console/logging/filters.py ================================================ from __future__ import annotations import logging POETRY_FILTER = logging.Filter(name="poetry") ================================================ FILE: src/poetry/console/logging/formatters/__init__.py ================================================ from __future__ import annotations from poetry.console.logging.formatters.builder_formatter import BuilderLogFormatter FORMATTERS = { "poetry.core.masonry.builders.builder": BuilderLogFormatter(), "poetry.core.masonry.builders.sdist": BuilderLogFormatter(), "poetry.core.masonry.builders.wheel": BuilderLogFormatter(), } ================================================ FILE: src/poetry/console/logging/formatters/builder_formatter.py ================================================ from __future__ import annotations import re from poetry.console.logging.formatters.formatter import Formatter class BuilderLogFormatter(Formatter): def format(self, msg: str) -> str: if msg.startswith("Building "): msg = re.sub("Building (.+)", " - Building \\1", msg) elif msg.startswith("Built "): msg = re.sub("Built (.+)", " - Built \\1", msg) elif msg.startswith("Adding: "): msg = re.sub("Adding: (.+)", " - Adding: \\1", msg) elif msg.startswith("Executing build script: "): msg = re.sub( "Executing build script: (.+)", " - Executing build script: \\1", msg, ) return msg ================================================ FILE: src/poetry/console/logging/formatters/formatter.py ================================================ from __future__ import annotations from abc import ABC from abc import abstractmethod class Formatter(ABC): @abstractmethod def format(self, msg: str) -> str: ... ================================================ FILE: src/poetry/console/logging/io_formatter.py ================================================ from __future__ import annotations import logging import sys import textwrap from pathlib import Path from typing import TYPE_CHECKING from typing import ClassVar from poetry.console.logging.filters import POETRY_FILTER from poetry.console.logging.formatters import FORMATTERS if TYPE_CHECKING: from logging import LogRecord class IOFormatter(logging.Formatter): _colors: ClassVar[dict[str, str]] = { "error": "fg=red", "warning": "fg=yellow", "debug": "debug", "info": "fg=blue", } def format(self, record: LogRecord) -> str: if not record.exc_info: level = record.levelname.lower() msg = record.msg if record.name in FORMATTERS: msg = FORMATTERS[record.name].format(msg) elif level in self._colors: msg = f"<{self._colors[level]}>{msg}" record.msg = msg formatted = super().format(record) if not POETRY_FILTER.filter(record): # prefix all lines from third-party packages for easier debugging formatted = textwrap.indent( formatted, f"[{_log_prefix(record)}] ", lambda line: True ) return formatted def _log_prefix(record: LogRecord) -> str: prefix = _path_to_package(Path(record.pathname)) or record.module if record.name != "root": prefix = ":".join([prefix, record.name]) return prefix def _path_to_package(path: Path) -> str | None: """Return main package name from the LogRecord.pathname.""" prefix: Path | None = None # Find the most specific prefix in sys.path. # We have to search the entire sys.path because a subsequent path might be # a sub path of the first match and thereby a better match. for syspath in sys.path: if ( prefix and prefix in (p := Path(syspath)).parents and p in path.parents ) or (not prefix and (p := Path(syspath)) in path.parents): prefix = p if not prefix: # this is unexpected, but let's play it safe return None path = path.relative_to(prefix) return path.parts[0] # main package name ================================================ FILE: src/poetry/console/logging/io_handler.py ================================================ from __future__ import annotations import logging from typing import TYPE_CHECKING if TYPE_CHECKING: from logging import LogRecord from cleo.io.io import IO class IOHandler(logging.Handler): def __init__(self, io: IO) -> None: self._io = io super().__init__() def emit(self, record: LogRecord) -> None: try: msg = self.format(record) level = record.levelname.lower() err = level in ("warning", "error", "exception", "critical") if err: self._io.write_error_line(msg) else: self._io.write_line(msg) except Exception: self.handleError(record) ================================================ FILE: src/poetry/exceptions.py ================================================ from __future__ import annotations class PoetryError(Exception): pass ================================================ FILE: src/poetry/factory.py ================================================ from __future__ import annotations import contextlib import logging import re from typing import TYPE_CHECKING from typing import Any from typing import cast from cleo.io.null_io import NullIO from packaging.utils import NormalizedName from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.constraints.version import parse_constraint from poetry.core.factory import Factory as BaseFactory from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.__version__ import __version__ from poetry.config.config import Config from poetry.exceptions import PoetryError from poetry.json import validate_object from poetry.packages.locker import Locker from poetry.plugins.plugin import Plugin from poetry.plugins.plugin_manager import PluginManager from poetry.poetry import Poetry from poetry.pyproject.toml import PyProjectTOML from poetry.toml.file import TOMLFile from poetry.utils.isolated_build import CONSTRAINTS_GROUP_NAME if TYPE_CHECKING: from collections.abc import Iterable from pathlib import Path from cleo.io.io import IO from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from tomlkit.toml_document import TOMLDocument from poetry.repositories import RepositoryPool from poetry.repositories.http_repository import HTTPRepository from poetry.utils.dependency_specification import DependencySpec logger = logging.getLogger(__name__) class Factory(BaseFactory): """ Factory class to create various elements needed by Poetry. """ def _ensure_valid_poetry_version(self, cwd: Path | None) -> None: poetry_file = self.locate(cwd) pyproject = PyProjectTOML(path=poetry_file) poetry_config = pyproject.data.get("tool", {}).get("poetry", {}) if version_str := poetry_config.get("requires-poetry"): version_constraint = parse_constraint(version_str) version = Version.parse(__version__) if not version_constraint.allows(version): raise PoetryError( f"This project requires Poetry {version_constraint}," f" but you are using Poetry {version}" ) def create_poetry( self, cwd: Path | None = None, with_groups: bool = True, io: IO | None = None, disable_plugins: bool = False, disable_cache: bool = False, ) -> Poetry: if io is None: io = NullIO() self._ensure_valid_poetry_version(cwd) base_poetry = super().create_poetry(cwd=cwd, with_groups=with_groups) build_constraints: dict[NormalizedName, list[Dependency]] = {} for name, constraints in base_poetry.local_config.get( "build-constraints", {} ).items(): name = canonicalize_name(name) build_constraints[name] = [] for dep_name, constraint in constraints.items(): _constraints = ( constraint if isinstance(constraint, list) else [constraint] ) for _constraint in _constraints: build_constraints[name].append( Factory.create_dependency( dep_name, _constraint, groups=[CONSTRAINTS_GROUP_NAME] ) ) poetry_file = base_poetry.pyproject_path locker = Locker(poetry_file.parent / "poetry.lock", base_poetry.pyproject.data) # Loading global configuration config = Config.create() # Loading local configuration local_config_file = TOMLFile(poetry_file.parent / "poetry.toml") if local_config_file.exists(): if io.is_debug(): io.write_line(f"Loading configuration file {local_config_file.path}") config.merge(local_config_file.read()) # Load local sources repositories = {} existing_repositories = config.get("repositories", {}) for source in base_poetry.local_config.get("source", []): name = source.get("name") url = source.get("url") if name and url and name not in existing_repositories: repositories[name] = {"url": url} config.merge({"repositories": repositories}) poetry = Poetry( poetry_file, base_poetry.local_config, base_poetry.package, locker, config, disable_cache=disable_cache, build_constraints=build_constraints, ) poetry.set_pool( self.create_pool( config, poetry.local_config.get("source", []), io, disable_cache=disable_cache, ) ) if not disable_plugins: plugin_manager = PluginManager(Plugin.group) plugin_manager.load_plugins() plugin_manager.activate(poetry, io) return poetry @classmethod def create_pool( cls, config: Config, sources: Iterable[dict[str, Any]] = (), io: IO | None = None, disable_cache: bool = False, ) -> RepositoryPool: from poetry.repositories import RepositoryPool from poetry.repositories.repository_pool import Priority if io is None: io = NullIO() if disable_cache: logger.debug("Disabling source caches") pool = RepositoryPool(config=config) explicit_pypi = False for source in sources: repository = cls.create_package_source( source, config, disable_cache=disable_cache ) priority = Priority[source.get("priority", Priority.PRIMARY.name).upper()] if io.is_debug(): io.write_line( f"Adding repository {repository.name} ({repository.url})" f" and setting it as {priority.name.lower()}" ) pool.add_repository(repository, priority=priority) if repository.name.lower() == "pypi": explicit_pypi = True # Only add PyPI if no primary repository is configured if not explicit_pypi: if pool.has_primary_repositories(): if io.is_debug(): io.write_line("Deactivating the PyPI repository") else: pool.add_repository( cls.create_package_source( {"name": "pypi"}, config, disable_cache=disable_cache ), priority=Priority.PRIMARY, ) if not pool.repositories: raise PoetryError( "At least one source must not be configured as 'explicit'." ) return pool @classmethod def create_package_source( cls, source: dict[str, str], config: Config, disable_cache: bool = False ) -> HTTPRepository: from poetry.repositories.exceptions import InvalidSourceError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.single_page_repository import SinglePageRepository try: name = source["name"] except KeyError: raise InvalidSourceError("Missing [name] in source.") pool_size = config.installer_max_workers if name.lower() == "pypi": if "url" in source: raise InvalidSourceError( "The PyPI repository cannot be configured with a custom url." ) return PyPiRepository( config=config, disable_cache=disable_cache, pool_size=pool_size, ) try: url = source["url"] except KeyError: raise InvalidSourceError(f"Missing [url] in source {name!r}.") repository_class = LegacyRepository if re.match(r".*\.(htm|html)$", url): repository_class = SinglePageRepository return repository_class( name, url, config=config, disable_cache=disable_cache, pool_size=pool_size, ) @classmethod def create_legacy_pyproject_from_package(cls, package: Package) -> TOMLDocument: import tomlkit from poetry.utils.dependency_specification import dependency_to_specification pyproject: dict[str, Any] = tomlkit.document() pyproject["tool"] = tomlkit.table(is_super_table=True) content: dict[str, Any] = tomlkit.table() pyproject["tool"]["poetry"] = content content["name"] = package.name content["version"] = package.version.text content["description"] = package.description content["authors"] = package.authors content["license"] = package.license.id if package.license else "" if package.classifiers: content["classifiers"] = package.classifiers if package.documentation_url: content["documentation"] = package.documentation_url if package.repository_url: content["repository"] = package.repository_url if package.homepage: content["homepage"] = package.homepage if package.maintainers: content["maintainers"] = package.maintainers if package.keywords: content["keywords"] = package.keywords readmes = [] for readme in package.readmes: readme_posix_path = readme.as_posix() with contextlib.suppress(ValueError): if package.root_dir: readme_posix_path = readme.relative_to(package.root_dir).as_posix() readmes.append(readme_posix_path) if readmes: content["readme"] = readmes optional_dependencies = set() extras_section = None if package.extras: extras_section = tomlkit.table() for extra in package.extras: _dependencies = [] for dependency in package.extras[extra]: _dependencies.append(dependency.name) optional_dependencies.add(dependency.name) extras_section[extra] = _dependencies optional_dependencies = set(optional_dependencies) dependency_section = content["dependencies"] = tomlkit.table() dependency_section["python"] = package.python_versions for dep in package.all_requires: constraint: DependencySpec | str = dependency_to_specification( dep, tomlkit.inline_table() ) if not isinstance(constraint, str): if dep.name in optional_dependencies: constraint["optional"] = True if len(constraint) == 1 and "version" in constraint: assert isinstance(constraint["version"], str) constraint = constraint["version"] elif not constraint: constraint = "*" for group in dep.groups: if group == MAIN_GROUP: dependency_section[dep.name] = constraint else: if "group" not in content: content["group"] = tomlkit.table(is_super_table=True) if group not in content["group"]: content["group"][group] = tomlkit.table(is_super_table=True) if "dependencies" not in content["group"][group]: content["group"][group]["dependencies"] = tomlkit.table() content["group"][group]["dependencies"][dep.name] = constraint if extras_section: content["extras"] = extras_section pyproject = cast("TOMLDocument", pyproject) return pyproject @classmethod def validate( cls, toml_data: dict[str, Any], strict: bool = False ) -> dict[str, list[str]]: results = super().validate(toml_data, strict) poetry_config = toml_data["tool"]["poetry"] results["errors"].extend( [e.replace("data.", "tool.poetry.") for e in validate_object(poetry_config)] ) # A project should not depend on itself. # TODO: consider [project.dependencies] and [project.optional-dependencies] dependencies = set(poetry_config.get("dependencies", {}).keys()) dependencies.update(poetry_config.get("dev-dependencies", {}).keys()) groups = poetry_config.get("group", {}).values() for group in groups: dependencies.update(group.get("dependencies", {}).keys()) dependencies = {canonicalize_name(d) for d in dependencies} project_name = toml_data.get("project", {}).get("name") or poetry_config.get( "name" ) if project_name is not None and canonicalize_name(project_name) in dependencies: results["errors"].append( f"Project name ({project_name}) is same as one of its dependencies" ) return results ================================================ FILE: src/poetry/inspection/__init__.py ================================================ ================================================ FILE: src/poetry/inspection/info.py ================================================ from __future__ import annotations import contextlib import functools import glob import logging import tempfile from pathlib import Path from tempfile import TemporaryDirectory from typing import TYPE_CHECKING from typing import Any import pkginfo from poetry.core.constraints.version import Version from poetry.core.factory import Factory from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.utils.helpers import parse_requires from poetry.core.version.markers import InvalidMarkerError from poetry.core.version.requirements import InvalidRequirementError from poetry.utils.helpers import extractall from poetry.utils.isolated_build import IsolatedBuildBackendError from poetry.utils.isolated_build import isolated_builder if TYPE_CHECKING: from collections.abc import Iterator from collections.abc import Sequence from packaging.metadata import RawMetadata from packaging.utils import NormalizedName from poetry.core.packages.package import PackageFile from poetry.core.packages.project_package import ProjectPackage logger = logging.getLogger(__name__) DYNAMIC_METADATA_VERSION = Version.parse("2.2") class PackageInfoError(ValueError): def __init__(self, path: Path, *reasons: BaseException | str) -> None: reasons = (f"Unable to determine package info for path: {path!s}", *reasons) super().__init__("\n\n".join(str(msg).strip() for msg in reasons if msg)) class PackageInfo: def __init__( self, *, name: str | None = None, version: str | None = None, summary: str | None = None, requires_dist: list[str] | None = None, requires_python: str | None = None, files: Sequence[PackageFile] | None = None, yanked: str | bool = False, cache_version: str | None = None, ) -> None: self.name = name self.version = version self.summary = summary self.requires_dist = requires_dist self.requires_python = requires_python self.files = files or [] self.yanked = yanked self._cache_version = cache_version self._source_type: str | None = None self._source_url: str | None = None self._source_reference: str | None = None @property def cache_version(self) -> str | None: return self._cache_version def update(self, other: PackageInfo) -> PackageInfo: self.name = other.name or self.name self.version = other.version or self.version self.summary = other.summary or self.summary self.requires_dist = other.requires_dist or self.requires_dist self.requires_python = other.requires_python or self.requires_python self.files = other.files or self.files self._cache_version = other.cache_version or self._cache_version return self def asdict(self) -> dict[str, Any]: """ Helper method to convert package info into a dictionary used for caching. """ return { "name": self.name, "version": self.version, "summary": self.summary, "requires_dist": self.requires_dist, "requires_python": self.requires_python, "files": self.files, "yanked": self.yanked, "_cache_version": self._cache_version, } @classmethod def load(cls, data: dict[str, Any]) -> PackageInfo: """ Helper method to load data from a dictionary produced by `PackageInfo.asdict()`. :param data: Data to load. This is expected to be a `dict` object output by `asdict()`. """ cache_version = data.pop("_cache_version", None) return cls(cache_version=cache_version, **data) def to_package( self, name: str | None = None, root_dir: Path | None = None ) -> Package: """ Create a new `poetry.core.packages.package.Package` instance using metadata from this instance. :param name: Name to use for the package, if not specified name from this instance is used. :param extras: Extras to activate for this package. :param root_dir: Optional root directory to use for the package. If set, dependency strings will be parsed relative to this directory. """ name = name or self.name if not name: raise RuntimeError(f"Unable to create package with no name for {root_dir}") if not self.version: # The version could not be determined, so we raise an error since it is # mandatory. raise RuntimeError(f"Unable to retrieve the package version for {name}") package = Package( name=name, version=self.version, source_type=self._source_type, source_url=self._source_url, source_reference=self._source_reference, yanked=self.yanked, ) if self.summary is not None: package.description = self.summary package.root_dir = root_dir package.python_versions = self.requires_python or "*" package.files = self.files # If this is a local poetry project, we can extract "richer" requirement # information, eg: development requirements etc. if root_dir is not None: path = root_dir elif self._source_type == "directory" and self._source_url is not None: path = Path(self._source_url) else: path = None if path is not None: poetry_package = self._get_poetry_package(path=path) if poetry_package: package.extras = poetry_package.extras for dependency in poetry_package.requires: package.add_dependency(dependency) return package seen_requirements = set() package_extras: dict[NormalizedName, list[Dependency]] = {} for req in self.requires_dist or []: try: # Attempt to parse the PEP-508 requirement string dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) except InvalidMarkerError: # Invalid marker, We strip the markers hoping for the best logger.warning( "Stripping invalid marker (%s) found in %s-%s dependencies", req, package.name, package.version, ) req = req.split(";")[0] dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) except InvalidRequirementError: # Unable to parse requirement so we skip it logger.warning( "Invalid requirement (%s) found in %s-%s dependencies, skipping", req, package.name, package.version, ) continue if dependency.in_extras: # this dependency is required by an extra package for extra in dependency.in_extras: if extra not in package_extras: # this is the first time we encounter this extra for this # package package_extras[extra] = [] package_extras[extra].append(dependency) req = dependency.to_pep_508(with_extras=True) if req not in seen_requirements: package.add_dependency(dependency) seen_requirements.add(req) package.extras = package_extras return package @classmethod def _requirements_from_distribution( cls, dist: pkginfo.BDist | pkginfo.SDist | pkginfo.Wheel, ) -> list[str] | None: """ Helper method to extract package requirements from a `pkginfo.Distribution` instance. :param dist: The distribution instance to extract requirements from. """ # If the distribution lists requirements, we use those. # # If the distribution does not list requirements, but the metadata is new enough # to specify that this is because there definitely are none: then we return an # empty list. # # If there is a requires.txt, we use that. if dist.requires_dist: return list(dist.requires_dist) if dist.metadata_version is not None: metadata_version = Version.parse(dist.metadata_version) if ( metadata_version >= DYNAMIC_METADATA_VERSION and "Requires-Dist" not in dist.dynamic ): return [] requires = Path(dist.filename) / "requires.txt" if requires.exists(): text = requires.read_text(encoding="utf-8") requirements = parse_requires(text) return requirements return None @classmethod def _from_distribution( cls, dist: pkginfo.BDist | pkginfo.SDist | pkginfo.Wheel ) -> PackageInfo: """ Helper method to parse package information from a `pkginfo.Distribution` instance. :param dist: The distribution instance to parse information from. """ # If the METADATA version is greater than the highest supported version, # pkginfo prints a warning and tries to parse the fields from the highest # known version. Assuming that METADATA versions adhere to semver, # this should be safe for minor updates. if not dist.metadata_version or dist.metadata_version.split(".")[0] not in { v.split(".")[0] for v in pkginfo.distribution.HEADER_ATTRS }: raise ValueError(f"Unknown metadata version: {dist.metadata_version}") requirements = cls._requirements_from_distribution(dist) info = cls( name=dist.name, version=dist.version, summary=dist.summary, requires_dist=requirements, requires_python=dist.requires_python, ) info._source_type = "file" info._source_url = Path(dist.filename).resolve().as_posix() return info @classmethod def _from_sdist_file(cls, path: Path) -> PackageInfo: """ Helper method to parse package information from an sdist file. We attempt to first inspect the file using `pkginfo.SDist`. If this does not provide us with package requirements, we extract the source and handle it as a directory. :param path: The sdist file to parse information from. """ info = None with contextlib.suppress(ValueError): sdist = pkginfo.SDist(str(path)) info = cls._from_distribution(sdist) if info is not None and info.requires_dist is not None: # we successfully retrieved dependencies from sdist metadata return info # Still not dependencies found # So, we unpack and introspect suffix = path.suffix zip = suffix == ".zip" if suffix == ".bz2": suffixes = path.suffixes if len(suffixes) > 1 and suffixes[-2] == ".tar": suffix = ".tar.bz2" elif not zip: suffix = ".tar.gz" with TemporaryDirectory(ignore_cleanup_errors=True) as tmp_str: tmp = Path(tmp_str) extractall(source=path, dest=tmp, zip=zip) # a little bit of guess work to determine the directory we care about elements = list(tmp.glob("*")) if len(elements) == 1 and elements[0].is_dir(): sdist_dir = elements[0] else: sdist_dir = tmp / path.name.rstrip(suffix) if not sdist_dir.is_dir(): sdist_dir = tmp # now this is an unpacked directory we know how to deal with new_info = cls.from_directory(path=sdist_dir) new_info._source_type = "file" new_info._source_url = path.resolve().as_posix() if not info: return new_info return info.update(new_info) @staticmethod def _find_dist_info(path: Path) -> Iterator[Path]: """ Discover all `*.*-info` directories in a given path. :param path: Path to search. """ pattern = "**/*.*-info" # Sometimes pathlib will fail on recursive symbolic links, so we need to work # around it and use the glob module instead. Note that this does not happen with # pathlib2 so it's safe to use it for Python < 3.4. directories = glob.iglob(path.joinpath(pattern).as_posix(), recursive=True) for d in directories: yield Path(d) @classmethod def from_metadata(cls, metadata: RawMetadata) -> PackageInfo: """ Create package information from core metadata. :param metadata: raw metadata """ return cls( name=metadata.get("name"), version=metadata.get("version"), summary=metadata.get("summary"), requires_dist=metadata.get("requires_dist"), requires_python=metadata.get("requires_python"), ) @classmethod def from_metadata_directory(cls, path: Path) -> PackageInfo | None: """ Helper method to parse package information from an unpacked metadata directory. :param path: The metadata directory to parse information from. """ if path.suffix in {".dist-info", ".egg-info"}: directories = [path] else: directories = list(cls._find_dist_info(path=path)) dist: pkginfo.BDist | pkginfo.SDist | pkginfo.Wheel for directory in directories: try: if directory.suffix == ".egg-info": dist = pkginfo.UnpackedSDist(directory.as_posix()) elif directory.suffix == ".dist-info": dist = pkginfo.Wheel(directory.as_posix()) else: continue break except ValueError: continue else: try: # handle PKG-INFO in unpacked sdist root dist = pkginfo.UnpackedSDist(path.as_posix()) except ValueError: return None return cls._from_distribution(dist=dist) @classmethod def from_package(cls, package: Package) -> PackageInfo: """ Helper method to inspect a `Package` object, in order to generate package info. :param package: This must be a poetry package instance. """ requires = {dependency.to_pep_508() for dependency in package.requires} for extra_requires in package.extras.values(): for dependency in extra_requires: requires.add(dependency.to_pep_508()) return cls( name=package.name, version=str(package.version), summary=package.description, requires_dist=list(requires), requires_python=package.python_versions, files=package.files, yanked=package.yanked_reason if package.yanked else False, ) @staticmethod def _get_poetry_package(path: Path) -> ProjectPackage | None: # Note: we ignore any setup.py file at this step # TODO: add support for handling non-poetry PEP-517 builds if PyProjectTOML(path.joinpath("pyproject.toml")).is_poetry_project(): with contextlib.suppress(RuntimeError): return Factory().create_poetry(path).package return None @classmethod def from_directory(cls, path: Path) -> PackageInfo: """ Generate package information from a package source directory. If introspection of all available metadata fails, the package is attempted to be built in an isolated environment so as to generate required metadata. :param path: Path to generate package information from. """ project_package = cls._get_poetry_package(path) info: PackageInfo | None if project_package: info = cls.from_package(project_package) else: info = cls.from_metadata_directory(path) if not info or info.requires_dist is None: try: info = get_pep517_metadata(path) except PackageInfoError: if not info: raise # we discovered PkgInfo but no requirements were listed info._source_type = "directory" info._source_url = path.as_posix() return info @classmethod def from_sdist(cls, path: Path) -> PackageInfo: """ Gather package information from an sdist file, packed or unpacked. :param path: Path to an sdist file or unpacked directory. """ if path.is_file(): return cls._from_sdist_file(path=path) # if we get here then it is neither an sdist instance nor a file # so, we assume this is an directory return cls.from_directory(path=path) @classmethod def from_wheel(cls, path: Path) -> PackageInfo: """ Gather package information from a wheel. :param path: Path to wheel. """ try: wheel = pkginfo.Wheel(str(path)) return cls._from_distribution(wheel) except ValueError as e: raise PackageInfoError(path, e) @classmethod def from_bdist(cls, path: Path) -> PackageInfo: """ Gather package information from a bdist (wheel etc.). :param path: Path to bdist. """ if path.suffix == ".whl": return cls.from_wheel(path=path) try: bdist = pkginfo.BDist(str(path)) return cls._from_distribution(bdist) except ValueError as e: raise PackageInfoError(path, e) @classmethod def from_path(cls, path: Path) -> PackageInfo: """ Gather package information from a given path (bdist, sdist, directory). :param path: Path to inspect. """ try: return cls.from_bdist(path=path) except PackageInfoError: return cls.from_sdist(path=path) @functools.cache def get_pep517_metadata(path: Path) -> PackageInfo: """ Helper method to use PEP-517 library to build and read package metadata. :param path: Path to package source to build and read metadata for. """ info = None with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as dist: try: dest = Path(dist) with isolated_builder(path, "wheel") as builder: builder.metadata_path(dest) info = PackageInfo.from_metadata_directory(dest) except IsolatedBuildBackendError as e: raise PackageInfoError(path, str(e)) from None if info: return info # if we reach here, everything has failed and all hope is lost raise PackageInfoError(path, "Exhausted all core metadata sources.") ================================================ FILE: src/poetry/inspection/lazy_wheel.py ================================================ """Lazy ZIP over HTTP""" from __future__ import annotations import io import logging import re from bisect import bisect_left from bisect import bisect_right from contextlib import contextmanager from tempfile import NamedTemporaryFile from typing import IO from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from urllib.parse import urlparse from zipfile import BadZipFile from zipfile import ZipFile from packaging.metadata import parse_email from requests.models import CONTENT_CHUNK_SIZE from requests.models import HTTPError from requests.models import Response from requests.status_codes import codes if TYPE_CHECKING: from collections.abc import Iterable from collections.abc import Iterator from types import TracebackType from packaging.metadata import RawMetadata from requests import Session from typing_extensions import Self from poetry.utils.authenticator import Authenticator logger = logging.getLogger(__name__) class LazyWheelUnsupportedError(Exception): """Raised when a lazy wheel is unsupported.""" class HTTPRangeRequestUnsupportedError(LazyWheelUnsupportedError): """Raised when the remote server appears unable to support byte ranges.""" class HTTPRangeRequestNotRespectedError(LazyWheelUnsupportedError): """Raised when the remote server tells us that it supports byte ranges but does not respect a respective request.""" class UnsupportedWheelError(LazyWheelUnsupportedError): """Unsupported wheel.""" class InvalidWheelError(LazyWheelUnsupportedError): """Invalid (e.g. corrupt) wheel.""" def __init__(self, location: str, name: str) -> None: self.location = location self.name = name def __str__(self) -> str: return f"Wheel {self.name} located at {self.location} is invalid." def metadata_from_wheel_url( name: str, url: str, session: Session | Authenticator ) -> RawMetadata: """Fetch metadata from the given wheel URL. This uses HTTP range requests to only fetch the portion of the wheel containing metadata, just enough for the object to be constructed. :raises HTTPRangeRequestUnsupportedError: if range requests are unsupported for ``url``. :raises InvalidWheelError: if the zip file contents could not be parsed. """ try: # After context manager exit, wheel.name will point to a deleted file path. # Add `delete_backing_file=False` to disable this for debugging. with LazyWheelOverHTTP(url, session) as lazy_file: metadata_bytes = lazy_file.read_metadata(name) metadata, _ = parse_email(metadata_bytes) return metadata except (BadZipFile, UnsupportedWheelError): # We assume that these errors have occurred because the wheel contents # themselves are invalid, not because we've messed up our bookkeeping # and produced an invalid file. raise InvalidWheelError(url, name) except Exception as e: if isinstance(e, LazyWheelUnsupportedError): # this is expected when the code handles issues with lazy wheel metadata retrieval correctly raise e logger.debug( "There was an unexpected %s when handling lazy wheel metadata retrieval for %s from %s: %s", type(e).__name__, name, url, e, ) # Catch all exception to handle any issues that may have occurred during # attempts to use Lazy Wheel. raise LazyWheelUnsupportedError( f"Attempts to use lazy wheel metadata retrieval for {name} from {url} failed" ) from e class MergeIntervals: """Stateful bookkeeping to merge interval graphs.""" def __init__(self, *, left: Iterable[int] = (), right: Iterable[int] = ()) -> None: self._left = list(left) self._right = list(right) def __repr__(self) -> str: return ( f"{type(self).__name__}" f"(left={tuple(self._left)}, right={tuple(self._right)})" ) def _merge( self, start: int, end: int, left: int, right: int ) -> Iterator[tuple[int, int]]: """Return an iterator of intervals to be fetched. Args: start: Start of needed interval end: End of needed interval left: Index of first overlapping downloaded data right: Index after last overlapping downloaded data """ lslice, rslice = self._left[left:right], self._right[left:right] i = start = min([start, *lslice[:1]]) end = max([end, *rslice[-1:]]) for j, k in zip(lslice, rslice): if j > i: yield i, j - 1 i = k + 1 if i <= end: yield i, end self._left[left:right], self._right[left:right] = [start], [end] def minimal_intervals_covering( self, start: int, end: int ) -> Iterator[tuple[int, int]]: """Provide the intervals needed to cover from ``start <= x <= end``. This method mutates internal state so that later calls only return intervals not covered by prior calls. The first call to this method will always return exactly one interval, which was exactly the one requested. Later requests for intervals overlapping that first requested interval will yield only the ranges not previously covered (which may be empty, e.g. if the same interval is requested twice). This may be used e.g. to download substrings of remote files on demand. """ left = bisect_left(self._right, start) right = bisect_right(self._left, end) yield from self._merge(start, end, left, right) class ReadOnlyIOWrapper(IO[bytes]): """Implement read-side ``IO[bytes]`` methods wrapping an inner ``IO[bytes]``. This wrapper is useful because Python currently does not distinguish read-only streams at the type level. """ def __init__(self, inner: IO[bytes]) -> None: self._file = inner def __enter__(self) -> Self: self._file.__enter__() return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: self._file.__exit__(exc_type, exc_value, traceback) def __iter__(self) -> Iterator[bytes]: raise NotImplementedError def __next__(self) -> bytes: raise NotImplementedError @property def mode(self) -> str: """Opening mode, which is always rb.""" return "rb" @property def name(self) -> str: """Path to the underlying file.""" return self._file.name def seekable(self) -> bool: """Return whether random access is supported, which is True.""" return True def close(self) -> None: """Close the file.""" self._file.close() @property def closed(self) -> bool: """Whether the file is closed.""" return self._file.closed def fileno(self) -> int: return self._file.fileno() def flush(self) -> None: self._file.flush() def isatty(self) -> bool: return False def readable(self) -> bool: """Return whether the file is readable, which is True.""" return True def read(self, size: int = -1) -> bytes: """Read up to size bytes from the object and return them. As a convenience, if size is unspecified or -1, all bytes until EOF are returned. Fewer than size bytes may be returned if EOF is reached. """ return self._file.read(size) def readline(self, limit: int = -1) -> bytes: # Explicit impl needed to satisfy mypy. raise NotImplementedError def readlines(self, hint: int = -1) -> list[bytes]: raise NotImplementedError def seek(self, offset: int, whence: int = 0) -> int: """Change stream position and return the new absolute position. Seek to offset relative position indicated by whence: * 0: Start of stream (the default). pos should be >= 0; * 1: Current position - pos may be negative; * 2: End of stream - pos usually negative. """ return self._file.seek(offset, whence) def tell(self) -> int: """Return the current position.""" return self._file.tell() def truncate(self, size: int | None = None) -> int: """Resize the stream to the given size in bytes. If size is unspecified resize to the current position. The current stream position isn't changed. Return the new file size. """ return self._file.truncate(size) def writable(self) -> bool: """Return False.""" return False def write(self, s: Any) -> int: raise NotImplementedError def writelines(self, lines: Iterable[Any]) -> None: raise NotImplementedError class LazyFileOverHTTP(ReadOnlyIOWrapper): """File-like object representing a fixed-length file over HTTP. This uses HTTP range requests to lazily fetch the file's content into a temporary file. If such requests are not supported by the server, raises ``HTTPRangeRequestUnsupportedError`` in the ``__enter__`` method.""" def __init__( self, url: str, session: Session | Authenticator, delete_backing_file: bool = True, ) -> None: inner = NamedTemporaryFile(delete=delete_backing_file) # noqa: SIM115 super().__init__(inner) self._merge_intervals: MergeIntervals | None = None self._length: int | None = None self._request_count = 0 self._session = session self._url = url def __enter__(self) -> Self: super().__enter__() self._setup_content() return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: self._reset_content() super().__exit__(exc_type, exc_value, traceback) def read(self, size: int = -1) -> bytes: """Read up to size bytes from the object and return them. As a convenience, if size is unspecified or -1, all bytes until EOF are returned. Fewer than size bytes may be returned if EOF is reached. :raises ValueError: if ``__enter__`` was not called beforehand. """ if self._length is None: raise ValueError(".__enter__() must be called to set up content length") cur = self.tell() logger.debug("read size %d at %d from lazy file %s", size, cur, self.name) if size < 0: assert cur <= self._length download_size = self._length - cur elif size == 0: return b"" else: download_size = size stop = min(cur + download_size, self._length) self._ensure_downloaded(cur, stop) return super().read(download_size) @classmethod def _uncached_headers(cls) -> dict[str, str]: """HTTP headers to bypass any HTTP caching. The requests we perform in this file are intentionally small, and any caching should be done at a higher level. Further, caching partial requests might cause issues: https://github.com/pypa/pip/pull/8716 """ # "no-cache" is the correct value for "up to date every time", so this will also # ensure we get the most recent value from the server: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#provide_up-to-date_content_every_time return {"Accept-Encoding": "identity", "Cache-Control": "no-cache"} def _setup_content(self) -> None: """Initialize the internal length field and other bookkeeping. Ensure ``self._merge_intervals`` is initialized. After parsing the remote file length with ``self._fetch_content_length()``, this method will truncate the underlying file from parent abstract class ``ReadOnlyIOWrapper`` to that size in order to support seek operations against ``io.SEEK_END`` in ``self.read()``. Called in ``__enter__``, and should make recursive invocations into a no-op. Subclasses may override this method.""" if self._merge_intervals is None: self._merge_intervals = MergeIntervals() if self._length is None: logger.debug("begin fetching content length") self._length = self._fetch_content_length() logger.debug("done fetching content length (is: %d)", self._length) # Enable us to seek and write anywhere in the backing file up to this # known length. self.truncate(self._length) else: logger.debug("content length already fetched (is: %d)", self._length) def _reset_content(self) -> None: """Unset the internal length field and merge intervals. Called in ``__exit__``, and should make recursive invocations into a no-op. Subclasses may override this method.""" if self._merge_intervals is not None: logger.debug( "unsetting merge intervals (were: %s)", repr(self._merge_intervals) ) self._merge_intervals = None if self._length is not None: logger.debug("unsetting content length (was: %d)", self._length) self._length = None def _content_length_from_head(self) -> int: """Performs a HEAD request to extract the Content-Length. :raises HTTPRangeRequestUnsupportedError: if the response fails to indicate support for "bytes" ranges.""" self._request_count += 1 head = self._session.head( self._url, headers=self._uncached_headers(), allow_redirects=True ) head.raise_for_status() assert head.status_code == codes.ok accepted_range = head.headers.get("Accept-Ranges", None) if accepted_range != "bytes": raise HTTPRangeRequestUnsupportedError( f"server does not support byte ranges: header was '{accepted_range}'" ) return int(head.headers["Content-Length"]) def _fetch_content_length(self) -> int: """Get the remote file's length.""" # NB: This is currently dead code, as _fetch_content_length() is overridden # again in LazyWheelOverHTTP. return self._content_length_from_head() def _stream_response(self, start: int, end: int) -> Response: """Return streaming HTTP response to a range request from start to end.""" headers = self._uncached_headers() headers["Range"] = f"bytes={start}-{end}" logger.debug("streamed bytes request: %s", headers["Range"]) self._request_count += 1 response = self._session.get(self._url, headers=headers, stream=True) try: response.raise_for_status() if int(response.headers["Content-Length"]) != (end - start + 1): raise HTTPRangeRequestNotRespectedError( f"server did not respect byte range request: " f"requested {end - start + 1} bytes, got " f"{response.headers['Content-Length']} bytes" ) return response except BaseException: response.close() raise def _fetch_content_range(self, start: int, end: int) -> Iterator[bytes]: """Perform a series of HTTP range requests to cover the specified byte range. NB: For compatibility with HTTP range requests, the range provided to this method must *include* the byte indexed at argument ``end`` (so e.g. ``0-1`` is 2 bytes long, and the range can never be empty). """ with self._stream_response(start, end) as response: yield from response.iter_content(CONTENT_CHUNK_SIZE) @contextmanager def _stay(self) -> Iterator[None]: """Return a context manager keeping the position. At the end of the block, seek back to original position. """ pos = self.tell() try: yield finally: self.seek(pos) def _ensure_downloaded(self, start: int, end: int) -> None: """Ensures bytes start to end (inclusive) have been downloaded and written to the backing file. :raises ValueError: if ``__enter__`` was not called beforehand. """ if self._merge_intervals is None: raise ValueError(".__enter__() must be called to set up merge intervals") # Reducing by 1 to get an inclusive end range. end -= 1 with self._stay(): for ( range_start, range_end, ) in self._merge_intervals.minimal_intervals_covering(start, end): self.seek(start) for chunk in self._fetch_content_range(range_start, range_end): self._file.write(chunk) class LazyWheelOverHTTP(LazyFileOverHTTP): """File-like object mapped to a ZIP file over HTTP. This uses HTTP range requests to lazily fetch the file's content, which should be provided as the first argument to a ``ZipFile``. """ # Cache this on the type to avoid trying and failing our initial lazy wheel request # multiple times in the same invocation against an index without this support. _domains_without_negative_range: ClassVar[set[str]] = set() _metadata_regex = re.compile(r"^[^/]*\.dist-info/METADATA$") def read_metadata(self, name: str) -> bytes: """Download and read the METADATA file from the remote wheel.""" with ZipFile(self) as zf: # prefetch metadata to reduce the number of range requests filename = self._prefetch_metadata(name) return zf.read(filename) @classmethod def _initial_chunk_length(cls) -> int: """Return the size of the chunk (in bytes) to download from the end of the file. This method is called in ``self._fetch_content_length()``. As noted in that method's docstring, this should be set high enough to cover the central directory sizes of the *average* wheels you expect to see, in order to avoid further requests before being able to process the zip file's contents at all. If we choose a small number, we need one more range request for larger wheels. If we choose a big number, we download unnecessary data from smaller wheels. If the chunk size from this method is larger than the size of an entire wheel, that may raise an HTTP error, but this is gracefully handled in ``self._fetch_content_length()`` with a small performance penalty. """ return 10_000 def _fetch_content_length(self) -> int: """Get the total remote file length, but also download a chunk from the end. This method is called within ``__enter__``. In an attempt to reduce the total number of requests needed to populate this lazy file's contents, this method will also attempt to fetch a chunk of the file's actual content. This chunk will be ``self._initial_chunk_length()`` bytes in size, or just the remote file's length if that's smaller, and the chunk will come from the *end* of the file. This method will first attempt to download with a negative byte range request, i.e. a GET with the headers ``Range: bytes=-N`` for ``N`` equal to ``self._initial_chunk_length()``. If negative offsets are unsupported, it will instead fall back to making a HEAD request first to extract the length, followed by a GET request with the double-ended range header ``Range: bytes=X-Y`` to extract the final ``N`` bytes from the remote resource. """ initial_chunk_size = self._initial_chunk_length() ret_length, tail = self._extract_content_length(initial_chunk_size) # Need to explicitly truncate here in order to perform the write and seek # operations below when we write the chunk of file contents to disk. self.truncate(ret_length) if tail is None: # If we could not download any file contents yet (e.g. if negative byte # ranges were not supported, or the requested range was larger than the file # size), then download all of this at once, hopefully pulling in the entire # central directory. initial_start = max(0, ret_length - initial_chunk_size) self._ensure_downloaded(initial_start, ret_length) else: # If we *could* download some file contents, then write them to the end of # the file and set up our bisect boundaries by hand. with self._stay(), tail: response_length = int(tail.headers["Content-Length"]) assert response_length == min(initial_chunk_size, ret_length) self.seek(-response_length, io.SEEK_END) # Default initial chunk size is currently 1MB, but streaming content # here allows it to be set arbitrarily large. for chunk in tail.iter_content(CONTENT_CHUNK_SIZE): self._file.write(chunk) # We now need to update our bookkeeping to cover the interval we just # wrote to file so we know not to do it in later read()s. init_chunk_start = ret_length - response_length # MergeIntervals uses inclusive boundaries i.e. start <= x <= end. init_chunk_end = ret_length - 1 assert self._merge_intervals is not None assert ((init_chunk_start, init_chunk_end),) == tuple( # NB: We expect LazyRemoteResource to reset `self._merge_intervals` # just before it calls the current method, so our assertion here # checks that indeed no prior overlapping intervals have # been covered. self._merge_intervals.minimal_intervals_covering( init_chunk_start, init_chunk_end ) ) return ret_length @staticmethod def _parse_full_length_from_content_range(arg: str) -> int: """Parse the file's full underlying length from the Content-Range header. This supports both * and numeric ranges, from success or error responses: https://www.rfc-editor.org/rfc/rfc9110#field.content-range. """ m = re.match(r"bytes [^/]+/([0-9]+)", arg) if m is None: raise HTTPRangeRequestUnsupportedError( f"could not parse Content-Range: '{arg}'" ) return int(m.group(1)) def _try_initial_chunk_request( self, initial_chunk_size: int ) -> tuple[int, Response]: """Attempt to fetch a chunk from the end of the file with a negative offset.""" headers = self._uncached_headers() # Perform a negative range index, which is not supported by some servers. headers["Range"] = f"bytes=-{initial_chunk_size}" logger.debug("initial bytes request: %s", headers["Range"]) self._request_count += 1 tail = self._session.get(self._url, headers=headers, stream=True) try: tail.raise_for_status() code = tail.status_code if code != codes.partial_content: # According to # https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests, # a 200 OK implies that range requests are not supported, # regardless of the requested size. # However, some servers that support negative range requests also return a # 200 OK if the requested range from the end was larger than the file size. if code == codes.ok: accept_ranges = tail.headers.get("Accept-Ranges", None) content_length = int(tail.headers["Content-Length"]) if ( accept_ranges == "bytes" and content_length <= initial_chunk_size ): return content_length, tail raise HTTPRangeRequestUnsupportedError( f"did not receive partial content: got code {code}" ) if "Content-Range" not in tail.headers: raise LazyWheelUnsupportedError( f"file length cannot be determined for {self._url}, " f"did not receive content range header from server" ) file_length = self._parse_full_length_from_content_range( tail.headers["Content-Range"] ) return (file_length, tail) except BaseException: tail.close() raise def _extract_content_length( self, initial_chunk_size: int ) -> tuple[int, Response | None]: """Get the Content-Length of the remote file, and possibly a chunk of it.""" domain = urlparse(self._url).netloc if domain in self._domains_without_negative_range: return (self._content_length_from_head(), None) tail: Response | None try: # Initial range request for just the end of the file. file_length, tail = self._try_initial_chunk_request(initial_chunk_size) except HTTPError as e: # Our initial request using a negative byte range was not supported. resp = e.response code = resp.status_code if resp is not None else None # This indicates that the requested range from the end was larger than the # actual file size: https://www.rfc-editor.org/rfc/rfc9110#status.416. if ( code == codes.requested_range_not_satisfiable and resp is not None and "Content-Range" in resp.headers ): # In this case, we don't have any file content yet, but we do know the # size the file will be, so we can return that and exit here. file_length = self._parse_full_length_from_content_range( resp.headers["Content-Range"] ) return file_length, None # pypi notably does not support negative byte ranges: see # https://github.com/pypi/warehouse/issues/12823. logger.debug( "Negative byte range not supported for domain '%s': " "using HEAD request before lazy wheel from now on (code: %s)", domain, code, ) # Avoid trying a negative byte range request against this domain for the # rest of the resolve. self._domains_without_negative_range.add(domain) # Apply a HEAD request to get the real size, and nothing else for now. return self._content_length_from_head(), None # Some servers that do not support negative offsets, # handle a negative offset like "-10" as "0-10"... # ... or behave even more strangely, see # https://github.com/python-poetry/poetry/issues/9056#issuecomment-1973273721 if int(tail.headers["Content-Length"]) > initial_chunk_size or tail.headers.get( "Content-Range", "" ).startswith("bytes -"): tail.close() tail = None self._domains_without_negative_range.add(domain) return file_length, tail def _prefetch_metadata(self, name: str) -> str: """Locate the *.dist-info/METADATA entry from a temporary ``ZipFile`` wrapper, and download it. This method assumes that the *.dist-info directory (containing e.g. METADATA) is contained in a single contiguous section of the zip file in order to ensure it can be downloaded in a single ranged GET request.""" logger.debug("begin prefetching METADATA for %s", name) start: int | None = None end: int | None = None # This may perform further requests if __init__() did not pull in the entire # central directory at the end of the file (although _initial_chunk_length() # should be set large enough to avoid this). zf = ZipFile(self) filename = "" for info in zf.infolist(): if start is None: if self._metadata_regex.search(info.filename): filename = info.filename start = info.header_offset continue else: # The last .dist-info/ entry may be before the end of the file if the # wheel's entries are sorted lexicographically (which is unusual). if not self._metadata_regex.search(info.filename): end = info.header_offset break if start is None: raise UnsupportedWheelError( f"no {self._metadata_regex!r} found for {name} in {self.name}" ) # If it is the last entry of the zip, then give us everything # until the start of the central directory. if end is None: end = zf.start_dir logger.debug(f"fetch {filename}") self._ensure_downloaded(start, end) logger.debug("done prefetching METADATA for %s", name) return filename ================================================ FILE: src/poetry/installation/__init__.py ================================================ from __future__ import annotations from poetry.installation.installer import Installer __all__ = ["Installer"] ================================================ FILE: src/poetry/installation/chef.py ================================================ from __future__ import annotations import tempfile from pathlib import Path from tempfile import TemporaryDirectory from typing import TYPE_CHECKING from poetry.utils.helpers import extractall from poetry.utils.isolated_build import isolated_builder if TYPE_CHECKING: from collections.abc import Mapping from collections.abc import Sequence from build import DistributionType from poetry.core.packages.dependency import Dependency from poetry.repositories import RepositoryPool from poetry.utils.cache import ArtifactCache from poetry.utils.env import Env class ChefError(Exception): ... class Chef: def __init__( self, artifact_cache: ArtifactCache, env: Env, pool: RepositoryPool ) -> None: self._env = env self._pool = pool self._artifact_cache = artifact_cache def prepare( self, archive: Path, output_dir: Path | None = None, *, editable: bool = False, config_settings: Mapping[str, str | Sequence[str]] | None = None, build_constraints: list[Dependency] | None = None, ) -> Path: if not self._should_prepare(archive): return archive if archive.is_dir(): destination = output_dir or Path(tempfile.mkdtemp(prefix="poetry-chef-")) return self._prepare( archive, destination=destination, editable=editable, config_settings=config_settings, build_constraints=build_constraints, ) return self._prepare_sdist( archive, destination=output_dir, config_settings=config_settings, build_constraints=build_constraints, ) def _prepare( self, directory: Path, destination: Path, *, editable: bool = False, config_settings: Mapping[str, str | Sequence[str]] | None = None, build_constraints: list[Dependency] | None = None, ) -> Path: distribution: DistributionType = "editable" if editable else "wheel" with isolated_builder( source=directory, distribution=distribution, python_executable=self._env.python, pool=self._pool, build_constraints=build_constraints, ) as builder: return Path( builder.build( distribution, destination.as_posix(), config_settings=config_settings, ) ) def _prepare_sdist( self, archive: Path, destination: Path | None = None, config_settings: Mapping[str, str | Sequence[str]] | None = None, build_constraints: list[Dependency] | None = None, ) -> Path: from poetry.core.packages.utils.link import Link suffix = archive.suffix zip = suffix == ".zip" with TemporaryDirectory(ignore_cleanup_errors=True) as tmp_dir: archive_dir = Path(tmp_dir) extractall(source=archive, dest=archive_dir, zip=zip) elements = list(archive_dir.glob("*")) if len(elements) == 1 and elements[0].is_dir(): sdist_dir = elements[0] else: sdist_dir = archive_dir / archive.name.rstrip(suffix) if not sdist_dir.is_dir(): sdist_dir = archive_dir if destination is None: destination = self._artifact_cache.get_cache_directory_for_link( Link(archive.as_uri()) ) destination.mkdir(parents=True, exist_ok=True) return self._prepare( sdist_dir, destination, config_settings=config_settings, build_constraints=build_constraints, ) def _should_prepare(self, archive: Path) -> bool: return archive.is_dir() or not self._is_wheel(archive) @classmethod def _is_wheel(cls, archive: Path) -> bool: return archive.suffix == ".whl" ================================================ FILE: src/poetry/installation/chooser.py ================================================ from __future__ import annotations import logging import re from typing import TYPE_CHECKING from typing import Any from poetry.config.config import Config from poetry.config.config import PackageFilterPolicy from poetry.console.exceptions import ConsoleMessage from poetry.console.exceptions import PoetryRuntimeError from poetry.repositories.http_repository import HTTPRepository from poetry.utils.helpers import get_highest_priority_hash_type from poetry.utils.wheel import Wheel if TYPE_CHECKING: from poetry.core.constraints.version import Version from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import Env logger = logging.getLogger(__name__) class Chooser: """ A Chooser chooses an appropriate release archive for packages. """ def __init__( self, pool: RepositoryPool, env: Env, config: Config | None = None ) -> None: self._pool = pool self._env = env self._config = config or Config.create() self._no_binary_policy: PackageFilterPolicy = PackageFilterPolicy( self._config.get("installer.no-binary", []) ) self._only_binary_policy: PackageFilterPolicy = PackageFilterPolicy( self._config.get("installer.only-binary", []) ) def choose_for(self, package: Package) -> Link: """ Return the url of the selected archive for a given package. """ links = [] # these are used only for providing insightful errors to the user unsupported_wheels = set() links_seen = 0 wheels_skipped = 0 sdists_skipped = 0 for link in self._get_links(package): links_seen += 1 if link.is_wheel: if ( # exact package name must reject wheel, even if `only-binary` includes it self._no_binary_policy.has_exact_package(package.name) # `:all:` reject wheel only if `only-binary` does not include it or ( not self._no_binary_policy.allows(package.name) and not self._only_binary_policy.has_exact_package(package.name) ) ): logger.debug( "Skipping wheel for %s as requested in no binary policy for" " package (%s)", link.filename, package.name, ) wheels_skipped += 1 continue if not Wheel(link.filename).is_supported_by_environment(self._env): logger.debug( "Skipping wheel %s as this is not supported by the current" " environment", link.filename, ) unsupported_wheels.add(link.filename) continue if link.ext in {".egg", ".exe", ".msi", ".rpm", ".srpm"}: logger.debug("Skipping unsupported distribution %s", link.filename) continue if link.is_sdist and ( # exact package name must reject sdist, even if `no-binary` includes it self._only_binary_policy.has_exact_package(package.name) # `:all:` reject sdist only if `no-binary` does not include it or ( not self._only_binary_policy.allows(package.name) and not self._no_binary_policy.has_exact_package(package.name) ) ): logger.debug( "Skipping source distribution for %s as requested in only binary policy for" " package (%s)", link.filename, package.name, ) sdists_skipped += 1 continue links.append(link) if not links: raise self._no_links_found_error( package, links_seen, wheels_skipped, sdists_skipped, unsupported_wheels ) # Get the best link chosen = max(links, key=lambda link: self._sort_key(package, link)) return chosen def _no_links_found_error( self, package: Package, links_seen: int, wheels_skipped: int, sdists_skipped: int, unsupported_wheels: set[str], ) -> PoetryRuntimeError: messages = [] info = ( f"This is likely not a Poetry issue.\n\n" f" - {links_seen} candidate(s) were identified for the package\n" ) if wheels_skipped > 0: info += f" - {wheels_skipped} wheel(s) were skipped due to your installer.no-binary policy\n" if sdists_skipped > 0: info += f" - {sdists_skipped} source distribution(s) were skipped due to your installer.only-binary policy\n" if unsupported_wheels: info += ( f" - {len(unsupported_wheels)} wheel(s) were skipped as your project's environment does not support " f"the identified abi tags\n" ) messages.append(ConsoleMessage(info.strip())) if unsupported_wheels: messages += [ ConsoleMessage( "The following wheel(s) were skipped as the current project environment does not support them " "due to abi compatibility issues.", debug=True, ), ConsoleMessage("\n".join(unsupported_wheels), debug=True) .indent(" - ") .wrap("warning"), ConsoleMessage( "If you would like to see the supported tags in your project environment, you can execute " "the following command:\n\n" " poetry debug tags", debug=True, ), ] source_hint = "" if package.source_type and package.source_reference: source_hint += f" ({package.source_reference})" messages.append( ConsoleMessage( f"Make sure the lockfile is up-to-date. You can try one of the following;\n\n" f" 1. Regenerate lockfile: poetry lock --no-cache --regenerate\n" f" 2. Update package : poetry update --no-cache {package.name}\n\n" # FIXME: In the future, it would be better to suggest a more targeted # cache clear command for just the package in question. E.g. # `poetry cache clear {package.source_reference}:{package.name}:{package.version}` # but `package.source_reference` currently resolves to `None` because # repository names are case sensitive at the moment (`PyPI` vs `pypi`). f"If any of those solutions worked, you will have to clear your caches using (poetry cache clear --all).\n\n" f"If neither works, please first check to verify that the {package.name} has published wheels " f"available from your configured source{source_hint} that are compatible with your environment" f"- ie. operating system, architecture (x86_64, arm64 etc.), python interpreter." ) .make_section("Solutions") .wrap("info") ) return PoetryRuntimeError( reason=f"Unable to find installation candidates for {package}", messages=messages, ) def _get_links(self, package: Package) -> list[Link]: if package.source_type: assert package.source_reference is not None repository = self._pool.repository(package.source_reference) elif not self._pool.has_repository("pypi"): repository = self._pool.repositories[0] else: repository = self._pool.repository("pypi") links = repository.find_links_for_package(package) locked_hashes = {f["hash"] for f in package.files} if not locked_hashes: return links selected_links = [] skipped = [] locked_hash_names = {h.split(":")[0] for h in locked_hashes} for link in links: if not link.hashes: selected_links.append(link) continue link_hash: str | None = None if (candidates := locked_hash_names.intersection(link.hashes.keys())) and ( hash_name := get_highest_priority_hash_type(candidates, link.filename) ): link_hash = f"{hash_name}:{link.hashes[hash_name]}" elif isinstance(repository, HTTPRepository): link_hash = repository.calculate_sha256(link) if link_hash not in locked_hashes: skipped.append((link.filename, link_hash)) logger.debug( "Skipping %s as %s checksum does not match expected value", link.filename, link_hash, ) continue selected_links.append(link) if links and not selected_links: reason = f"Downloaded distributions for {package.pretty_name} ({package.pretty_version}) did not match any known checksums in your lock file." link_hashes = "\n".join(f" - {link}({h})" for link, h in skipped) known_hashes = "\n".join(f" - {h}" for h in locked_hashes) messages = [ ConsoleMessage( "Causes:\n" " - invalid or corrupt cache either during locking or installation\n" " - network interruptions or errors causing corrupted downloads\n\n" "Solutions:\n" " 1. Try running your command again using the --no-cache global option enabled.\n" " 2. Try regenerating your lock file using (poetry lock --no-cache --regenerate).\n\n" "If any of those solutions worked, you will have to clear your caches using (poetry cache clear --all CACHE_NAME)." ), ConsoleMessage( f"Poetry retrieved the following links:\n" f"{link_hashes}\n\n" f"The lockfile contained only the following hashes:\n" f"{known_hashes}", debug=True, ), ] raise PoetryRuntimeError(reason, messages) return selected_links def _sort_key( self, package: Package, link: Link ) -> tuple[int, int, int, Version, tuple[Any, ...], int]: """ Function to pass as the `key` argument to a call to sorted() to sort InstallationCandidates by preference. Returns a tuple such that tuples sorting as greater using Python's default comparison operator are more preferred. The preference is as follows: First and foremost, candidates with allowed (matching) hashes are always preferred over candidates without matching hashes. This is because e.g. if the only candidate with an allowed hash is yanked, we still want to use that candidate. Second, excepting hash considerations, candidates that have been yanked (in the sense of PEP 592) are always less preferred than candidates that haven't been yanked. Then: If not finding wheels, they are sorted by version only. If finding wheels, then the sort order is by version, then: 1. existing installs 2. wheels ordered via Wheel.support_index_min(self._supported_tags) 3. source archives If prefer_binary was set, then all wheels are sorted above sources. Note: it was considered to embed this logic into the Link comparison operators, but then different sdist links with the same version, would have to be considered equal """ build_tag: tuple[Any, ...] = () binary_preference = 0 if link.is_wheel: wheel = Wheel(link.filename) if not wheel.is_supported_by_environment(self._env): raise RuntimeError( f"{wheel.filename} is not a supported wheel for this platform. It " "can't be sorted." ) # TODO: Binary preference pri = -(wheel.get_minimum_supported_index(self._env.supported_tags) or 0) if wheel.build_tag is not None: match = re.match(r"^(\d+)(.*)$", wheel.build_tag) if not match: raise ValueError(f"Unable to parse build tag: {wheel.build_tag}") build_tag_groups = match.groups() build_tag = (int(build_tag_groups[0]), build_tag_groups[1]) else: # sdist support_num = len(self._env.supported_tags) pri = -support_num has_allowed_hash = int(self._is_link_hash_allowed_for_package(link, package)) yank_value = int(not link.yanked) return ( has_allowed_hash, yank_value, binary_preference, package.version, build_tag, pri, ) def _is_link_hash_allowed_for_package(self, link: Link, package: Package) -> bool: if not link.hashes: return True link_hashes = {f"{name}:{h}" for name, h in link.hashes.items()} locked_hashes = {f["hash"] for f in package.files} return bool(link_hashes & locked_hashes) ================================================ FILE: src/poetry/installation/executor.py ================================================ from __future__ import annotations import csv import functools import itertools import json import threading from collections import defaultdict from concurrent.futures import ThreadPoolExecutor from concurrent.futures import wait from pathlib import Path from typing import TYPE_CHECKING from typing import Any from poetry.core.packages.utils.link import Link from poetry.console.exceptions import PoetryRuntimeError from poetry.installation.chef import Chef from poetry.installation.chooser import Chooser from poetry.installation.operations import Install from poetry.installation.operations import Uninstall from poetry.installation.operations import Update from poetry.installation.wheel_installer import WheelInstaller from poetry.puzzle.exceptions import SolverProblemError from poetry.utils._compat import decode from poetry.utils.authenticator import Authenticator from poetry.utils.env import EnvCommandError from poetry.utils.helpers import Downloader from poetry.utils.helpers import get_file_hash from poetry.utils.helpers import get_highest_priority_hash_type from poetry.utils.helpers import pluralize from poetry.utils.helpers import remove_directory from poetry.utils.isolated_build import IsolatedBuildBackendError from poetry.utils.isolated_build import IsolatedBuildInstallError from poetry.utils.log_utils import format_build_wheel_log from poetry.vcs.git import Git if TYPE_CHECKING: from collections.abc import Mapping from collections.abc import Sequence from cleo.io.io import IO from cleo.io.outputs.section_output import SectionOutput from packaging.utils import NormalizedName from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.config.config import Config from poetry.installation.operations.operation import Operation from poetry.repositories import RepositoryPool from poetry.utils.env import Env def _package_get_name(package: Package) -> str | None: if url := package.repository_url: return Git.get_name_from_source_url(url) return None class Executor: def __init__( self, env: Env, pool: RepositoryPool, config: Config, io: IO, parallel: bool | None = None, disable_cache: bool = False, *, build_constraints: Mapping[NormalizedName, list[Dependency]] | None = None, ) -> None: self._env = env self._io = io self._dry_run = False self._enabled = True self._verbose = False self._wheel_installer = WheelInstaller(self._env) self._build_constraints = build_constraints or {} if parallel is None: parallel = config.get("installer.parallel", True) if parallel: self._max_workers = config.installer_max_workers else: self._max_workers = 1 self._artifact_cache = pool.artifact_cache self._authenticator = Authenticator( config, self._io, disable_cache=disable_cache, pool_size=self._max_workers ) self._chef = Chef(self._artifact_cache, self._env, pool) self._chooser = Chooser(pool, self._env, config) self._executor = ThreadPoolExecutor(max_workers=self._max_workers) self._executed = {"install": 0, "update": 0, "uninstall": 0} self._skipped = {"install": 0, "update": 0, "uninstall": 0} self._sections: dict[int, SectionOutput] = {} self._yanked_warnings: list[str] = [] self._lock = threading.Lock() self._shutdown = False self._hashes: dict[str, str] = {} # Cache whether decorated output is supported. # https://github.com/python-poetry/cleo/issues/423 self._decorated_output: bool = self._io.output.is_decorated() self._max_retries = config.get("requests.max-retries", 0) # sdist build config settings self._build_config_settings: Mapping[ NormalizedName, Mapping[str, str | Sequence[str]] ] = config.get("installer.build-config-settings") @property def installations_count(self) -> int: return self._executed["install"] @property def updates_count(self) -> int: return self._executed["update"] @property def removals_count(self) -> int: return self._executed["uninstall"] @property def enabled(self) -> bool: return self._enabled def supports_fancy_output(self) -> bool: return self._decorated_output and not self._dry_run def disable(self) -> Executor: self._enabled = False return self def dry_run(self, dry_run: bool = True) -> Executor: self._dry_run = dry_run return self def verbose(self, verbose: bool = True) -> Executor: self._verbose = verbose return self def enable_bytecode_compilation(self, enable: bool = True) -> None: self._wheel_installer.enable_bytecode_compilation(enable) def execute(self, operations: list[Operation]) -> int: for job_type in self._executed: self._executed[job_type] = 0 self._skipped[job_type] = 0 if operations and (self._enabled or self._dry_run): self._display_summary(operations) self._sections = {} self._yanked_warnings = [] # pip has to be installed/updated first without parallelism # because we still need it for uninstalls for i, op in enumerate(operations): if op.package.name == "pip": wait([self._executor.submit(self._execute_operation, op)]) del operations[i] break # We group operations by priority groups = itertools.groupby(operations, key=lambda o: -o.priority) for _, group in groups: tasks = [] serial_operations = [] serial_git_operations = defaultdict(list) for operation in group: if self._shutdown: break # Some operations are unsafe, we must execute them serially in a group # https://github.com/python-poetry/poetry/issues/3086 # https://github.com/python-poetry/poetry/issues/2658 # # We need to explicitly check source type here, see: # https://github.com/python-poetry/poetry-core/pull/98 is_parallel_unsafe = operation.job_type == "uninstall" or ( operation.package.develop and operation.package.source_type in {"directory", "git"} ) # Skipped operations are safe to execute in parallel if operation.skipped: is_parallel_unsafe = False if is_parallel_unsafe: serial_operations.append(operation) elif operation.package.source_type == "git": # Serially execute git operations that get cloned to the same directory, # to prevent multiple parallel git operations in the same repo. serial_git_operations[_package_get_name(operation.package)].append( operation ) else: tasks.append( self._executor.submit(self._execute_operation, operation) ) def _serialize( repository_serial_operations: list[Operation], ) -> None: for operation in repository_serial_operations: self._execute_operation(operation) # For each git repository, execute all operations serially for repository_git_operations in serial_git_operations.values(): tasks.append( self._executor.submit( _serialize, repository_serial_operations=repository_git_operations, ) ) try: wait(tasks) for operation in serial_operations: self._execute_operation(operation) except KeyboardInterrupt: self._shutdown = True if self._shutdown: self._executor.shutdown(wait=True, cancel_futures=True) break for warning in self._yanked_warnings: self._io.write_error_line(f"Warning: {warning}") for path, issues in self._wheel_installer.invalid_wheels.items(): formatted_issues = "\n".join(issues) warning = ( f"Validation of the RECORD file of {path.name} failed." " Please report to the maintainers of that package so they can fix" f" their build process. Details:\n{formatted_issues}\n" ) self._io.write_error_line(f"Warning: {warning}") return 1 if self._shutdown else 0 def _write(self, operation: Operation, line: str) -> None: if not self.supports_fancy_output() or not self._should_write_operation( operation ): return if self._io.is_debug(): with self._lock: section = self._sections[id(operation)] section.write_line(line) return with self._lock: section = self._sections[id(operation)] section.clear() section.write(line) def _execute_operation(self, operation: Operation) -> None: try: op_message = self.get_operation_message(operation) if self.supports_fancy_output(): if id(operation) not in self._sections and self._should_write_operation( operation ): with self._lock: self._sections[id(operation)] = self._io.section() self._sections[id(operation)].write_line( f" - {op_message}:" " Pending..." ) else: if self._should_write_operation(operation): if not operation.skipped: self._io.write_line( f" - {op_message}" ) else: self._io.write_line( f" - {op_message}: " "Skipped " "for the following reason: " f"{operation.skip_reason}" ) try: result = self._do_execute_operation(operation) except EnvCommandError as e: if e.e.returncode == -2: result = -2 else: raise # If we have a result of -2 it means a KeyboardInterrupt # in the any python subprocess, so we raise a KeyboardInterrupt # error to be picked up by the error handler. if result == -2: raise KeyboardInterrupt except Exception as e: try: from cleo.ui.exception_trace import ExceptionTrace io: IO | SectionOutput if not self.supports_fancy_output(): io = self._io else: message = ( " -" f" {self.get_operation_message(operation, error=True)}:" " Failed" ) self._write(operation, message) io = self._sections.get(id(operation), self._io) with self._lock: pkg = operation.package with_trace = True if isinstance(e, IsolatedBuildBackendError): # TODO: Revisit once upstream fix is available https://github.com/python-poetry/cleo/issues/454 # we disable trace here explicitly to workaround incorrect context detection by crashtest with_trace = False pip_command = "pip wheel --no-cache-dir --use-pep517" if pkg.develop: if pkg.source_type == "git": git_url_parts = ( pkg.to_dependency() .to_pep_508() .split(";", 1)[0] .split("@", 1)[-1] .strip() ).split("#", 1) requirement = f"{git_url_parts[0]}#egg={pkg.name}" if len(git_url_parts) > 1: requirement += f"&{git_url_parts[1]}" else: assert pkg.source_url requirement = pkg.source_url pip_command += " --editable" else: requirement = ( pkg.to_dependency().to_pep_508().split(";")[0].strip() ) if config_settings := self._build_config_settings.get(pkg.name): for setting in config_settings: for setting_value in config_settings[setting]: pip_command += f" --config-settings='{setting}={setting_value}'" message = e.generate_message( source_string=f"{pkg.pretty_name} ({pkg.full_pretty_version})", build_command=f'{pip_command} "{requirement}"', ) elif isinstance(e, IsolatedBuildInstallError): message = ( "" "Cannot install build-system.requires" f" for {pkg.pretty_name}." "" ) elif isinstance(e, SolverProblemError): message = ( "" "Cannot resolve build-system.requires" f" for {pkg.pretty_name}." "" ) elif isinstance(e, PoetryRuntimeError): message = e.get_text(io.is_verbose(), indent=" | ").rstrip() message = f"{message}" with_trace = False else: message = f"Cannot install {pkg.pretty_name}." if with_trace: ExceptionTrace(e).render(io) io.write_line("") io.write_line(message) io.write_line("") finally: with self._lock: self._shutdown = True except KeyboardInterrupt: try: message = ( " -" f" {self.get_operation_message(operation, warning=True)}:" " Cancelled" ) if not self.supports_fancy_output(): self._io.write_line(message) else: self._write(operation, message) finally: with self._lock: self._shutdown = True def _do_execute_operation(self, operation: Operation) -> int: method = operation.job_type operation_message = self.get_operation_message(operation) if operation.skipped: if self.supports_fancy_output(): self._write( operation, f" - {operation_message}: " "Skipped " "for the following reason: " f"{operation.skip_reason}", ) self._skipped[operation.job_type] += 1 return 0 if not self._enabled or self._dry_run: return 0 result: int = getattr(self, f"_execute_{method}")(operation) if result != 0: return result operation_message = self.get_operation_message(operation, done=True) message = f" - {operation_message}" self._write(operation, message) self._increment_operations_count(operation, True) return result def _increment_operations_count(self, operation: Operation, executed: bool) -> None: with self._lock: if executed: self._executed[operation.job_type] += 1 else: self._skipped[operation.job_type] += 1 def run_pip(self, *args: Any, **kwargs: Any) -> int: try: self._env.run_pip(*args, **kwargs) except EnvCommandError as e: output = decode(e.e.output) if ( "KeyboardInterrupt" in output or "ERROR: Operation cancelled by user" in output ): return -2 raise return 0 def get_operation_message( self, operation: Operation, done: bool = False, error: bool = False, warning: bool = False, ) -> str: base_tag = "fg=default" operation_color = "c2" source_operation_color = "c2" package_color = "c1" if error: operation_color = "error" elif warning: operation_color = "warning" elif done: operation_color = "success" if operation.skipped: base_tag = "fg=default;options=dark" operation_color += "_dark" source_operation_color += "_dark" package_color += "_dark" if isinstance(operation, Install): return ( f"<{base_tag}>Installing" f" <{package_color}>{operation.package.name}" f" (<{operation_color}>{operation.package.full_pretty_version})" ) if isinstance(operation, Uninstall): return ( f"<{base_tag}>Removing" f" <{package_color}>{operation.package.name}" f" (<{operation_color}>{operation.package.full_pretty_version})" ) if isinstance(operation, Update): initial_version = (initial_pkg := operation.initial_package).version target_version = (target_pkg := operation.target_package).version update_kind = ( "Updating" if target_version >= initial_version else "Downgrading" ) return ( f"<{base_tag}>{update_kind}" f" <{package_color}>{initial_pkg.name} " f"(<{source_operation_color}>" f"{initial_pkg.full_pretty_version}" f" -> <{operation_color}>" f"{target_pkg.full_pretty_version})" ) return "" def _display_summary(self, operations: list[Operation]) -> None: installs = 0 updates = 0 uninstalls = 0 skipped = 0 for op in operations: if op.skipped: skipped += 1 continue if op.job_type == "install": installs += 1 elif op.job_type == "update": updates += 1 elif op.job_type == "uninstall": uninstalls += 1 if not installs and not updates and not uninstalls and not self._verbose: self._io.write_line("") self._io.write_line("No dependencies to install or update") return self._io.write_line("") self._io.write("Package operations: ") self._io.write(f"{installs} install{pluralize(installs)}, ") self._io.write(f"{updates} update{pluralize(updates)}, ") self._io.write(f"{uninstalls} removal{pluralize(uninstalls)}") if skipped and self._verbose: self._io.write(f", {skipped} skipped") self._io.write_line("") self._io.write_line("") def _execute_install(self, operation: Install | Update) -> int: status_code = self._install(operation) self._save_url_reference(operation) return status_code def _execute_update(self, operation: Install | Update) -> int: status_code = self._update(operation) self._save_url_reference(operation) return status_code def _execute_uninstall(self, operation: Uninstall) -> int: op_msg = self.get_operation_message(operation) message = f" - {op_msg}: Removing..." self._write(operation, message) return self._remove(operation.package) def _install(self, operation: Install | Update) -> int: package = operation.package cleanup_archive: bool = False if package.source_type == "git": archive = self._prepare_git_archive(operation) cleanup_archive = operation.package.develop elif package.source_type == "file": archive = self._prepare_archive(operation) elif package.source_type == "directory": archive = self._prepare_archive(operation) cleanup_archive = True elif package.source_type == "url": assert package.source_url is not None archive = self._download_link(operation, Link(package.source_url)) else: archive = self._download(operation) operation_message = self.get_operation_message(operation) message = ( f" - {operation_message}:" " Installing..." ) self._write(operation, message) try: if operation.job_type == "update": # Uninstall first # TODO: Make an uninstaller and find a way to rollback in case # the new package can't be installed assert isinstance(operation, Update) self._remove(operation.initial_package) self._wheel_installer.install(archive) finally: if cleanup_archive: archive.unlink() return 0 def _update(self, operation: Install | Update) -> int: return self._install(operation) def _remove(self, package: Package) -> int: # If we have a VCS package, remove its source directory if package.source_type == "git": src_dir = self._env.path / "src" / package.name if src_dir.exists(): remove_directory(src_dir, force=True) try: return self.run_pip("uninstall", package.name, "-y") except EnvCommandError as e: if "not installed" in str(e): return 0 raise def _prepare_archive( self, operation: Install | Update, *, output_dir: Path | None = None ) -> Path: package = operation.package operation_message = self.get_operation_message(operation) message = ( f" - {operation_message}:" f"{format_build_wheel_log(package, self._env)}" ) self._write(operation, message) assert package.source_url is not None archive = Path(package.source_url) if package.source_subdirectory: archive = archive / package.source_subdirectory if not Path(package.source_url).is_absolute() and package.root_dir: archive = package.root_dir / archive self._populate_hashes_dict(archive, package) name = operation.package.name return self._chef.prepare( archive, editable=package.develop, output_dir=output_dir, config_settings=self._build_config_settings.get(name), build_constraints=self._build_constraints.get(name), ) def _prepare_git_archive(self, operation: Install | Update) -> Path: package = operation.package assert package.source_url is not None if package.source_resolved_reference and not package.develop: # Only cache git archives when we know precise reference hash, # otherwise we might get stale archives cached_archive = self._artifact_cache.get_cached_archive_for_git( package.source_url, package.source_resolved_reference, package.source_subdirectory, env=self._env, ) if cached_archive is not None: return cached_archive operation_message = self.get_operation_message(operation) message = ( f" - {operation_message}: Cloning..." ) self._write(operation, message) source = Git.clone( url=package.source_url, source_root=self._env.path / "src", revision=package.source_resolved_reference or package.source_reference, ) # Now we just need to install from the source directory original_url = package.source_url package._source_url = str(source.path) output_dir = None if package.source_resolved_reference and not package.develop: output_dir = self._artifact_cache.get_cache_directory_for_git( original_url, package.source_resolved_reference, package.source_subdirectory, ) try: archive = self._prepare_archive(operation, output_dir=output_dir) except Exception: # always reset source_url in case of an error for correct output package._source_url = original_url raise if not package.develop: package._source_url = original_url if output_dir is not None and output_dir.is_dir(): # Mark directories with cached git packages, to distinguish from # "normal" cache (output_dir / ".created_from_git_dependency").touch() return archive def _download(self, operation: Install | Update) -> Path: link = self._chooser.choose_for(operation.package) if link.yanked: # Store yanked warnings in a list and print after installing, so they can't # be overlooked. Further, printing them in the concerning section would have # the risk of overwriting the warning, so it is only briefly visible. message = ( f"The file chosen for install of {operation.package.pretty_name} " f"{operation.package.pretty_version} ({link.show_url}) is yanked." ) if link.yanked_reason: message += f" Reason for being yanked: {link.yanked_reason}" self._yanked_warnings.append(message) return self._download_link(operation, link) def _download_link(self, operation: Install | Update, link: Link) -> Path: package = operation.package # Get original package for the link provided download_func = functools.partial(self._download_archive, operation) original_archive = self._artifact_cache.get_cached_archive_for_link( link, strict=True, download_func=download_func ) # Get potential higher prioritized cached archive, otherwise it will fall back # to the original archive. archive = self._artifact_cache.get_cached_archive_for_link( link, strict=False, env=self._env, ) if archive is None: # Since we previously downloaded an archive, we now should have # something cached that we can use here. The only case in which # archive is None is if the original archive is not valid for the # current environment. raise RuntimeError( f"Package {link.url} cannot be installed in the current environment" f" {self._env.marker_env}" ) if archive.suffix != ".whl": message = ( f" - {self.get_operation_message(operation)}:" f"{format_build_wheel_log(package, self._env)}" ) self._write(operation, message) name = operation.package.name archive = self._chef.prepare( archive, output_dir=original_archive.parent, config_settings=self._build_config_settings.get(name), build_constraints=self._build_constraints.get(name), ) # Use the original archive to provide the correct hash. self._populate_hashes_dict(original_archive, package) return archive def _populate_hashes_dict(self, archive: Path, package: Package) -> None: if package.files and archive.name in {f["file"] for f in package.files}: archive_hash = self._validate_archive_hash(archive, package) self._hashes[package.name] = archive_hash @staticmethod def _validate_archive_hash(archive: Path, package: Package) -> str: known_hashes = {f["hash"] for f in package.files if f["file"] == archive.name} hash_types = {t.split(":")[0] for t in known_hashes} hash_type = get_highest_priority_hash_type(hash_types, archive.name) if hash_type is None: raise RuntimeError( f"No usable hash type(s) for {package} from archive" f" {archive.name} found (known hashes: {known_hashes!s})" ) archive_hash = f"{hash_type}:{get_file_hash(archive, hash_type)}" if archive_hash not in known_hashes: raise RuntimeError( f"Hash for {package} from archive {archive.name} not found in" f" known hashes (was: {archive_hash})" ) return archive_hash def _download_archive( self, operation: Install | Update, url: str, dest: Path, ) -> None: downloader = Downloader( url, dest, self._authenticator, max_retries=self._max_retries ) wheel_size = downloader.total_size operation_message = self.get_operation_message(operation) message = ( f" - {operation_message}: Downloading..." ) progress = None if self.supports_fancy_output(): if wheel_size is None: self._write(operation, message) else: from cleo.ui.progress_bar import ProgressBar progress = ProgressBar( self._sections[id(operation)], max=int(wheel_size) ) progress.set_format(message + " %percent%%") if progress: with self._lock: self._sections[id(operation)].clear() progress.start() for fetched_size in downloader.download_with_progress(chunk_size=4096): if progress: with self._lock: progress.set_progress(fetched_size) if progress: with self._lock: progress.finish() def _should_write_operation(self, operation: Operation) -> bool: return ( not operation.skipped or self._dry_run or self._verbose or not self._enabled ) def _save_url_reference(self, operation: Operation) -> None: """ Create and store a PEP-610 `direct_url.json` file, if needed. """ if operation.job_type not in {"install", "update"}: return package = operation.package if not package.source_url or package.source_type == "legacy": return url_reference: dict[str, Any] | None = None if package.source_type == "git" and not package.develop: url_reference = self._create_git_url_reference(package) elif package.source_type in ("directory", "git"): url_reference = self._create_directory_url_reference(package) elif package.source_type == "url": url_reference = self._create_url_url_reference(package) elif package.source_type == "file": url_reference = self._create_file_url_reference(package) if url_reference: for dist in self._env.site_packages.distributions( name=package.name, writable_only=True ): dist_path = dist._path # type: ignore[attr-defined] assert isinstance(dist_path, Path) url = dist_path / "direct_url.json" url.write_text(json.dumps(url_reference), encoding="utf-8") record = dist_path / "RECORD" if record.exists(): with record.open(mode="a", encoding="utf-8", newline="") as f: writer = csv.writer(f) path = url.relative_to(record.parent.parent) writer.writerow([str(path), "", ""]) def _create_git_url_reference(self, package: Package) -> dict[str, Any]: reference = { "url": package.source_url, "vcs_info": { "vcs": "git", "requested_revision": package.source_reference, "commit_id": package.source_resolved_reference, }, } if package.source_subdirectory: reference["subdirectory"] = package.source_subdirectory return reference def _create_url_url_reference(self, package: Package) -> dict[str, Any]: archive_info = self._get_archive_info(package) return {"url": package.source_url, "archive_info": archive_info} def _create_file_url_reference(self, package: Package) -> dict[str, Any]: archive_info = self._get_archive_info(package) assert package.source_url is not None return { "url": Path(package.source_url).as_uri(), "archive_info": archive_info, } def _create_directory_url_reference(self, package: Package) -> dict[str, Any]: dir_info = {} if package.develop: dir_info["editable"] = True assert package.source_url is not None return { "url": Path(package.source_url).as_uri(), "dir_info": dir_info, } def _get_archive_info(self, package: Package) -> dict[str, Any]: """ Create dictionary `archive_info` for file `direct_url.json`. Specification: https://packaging.python.org/en/latest/specifications/direct-url (it supersedes PEP 610) :param package: This must be a poetry package instance. """ archive_info = {} if package.name in self._hashes: algorithm, value = self._hashes[package.name].split(":") archive_info["hashes"] = {algorithm: value} return archive_info ================================================ FILE: src/poetry/installation/installer.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import cast from cleo.io.null_io import NullIO from packaging.utils import canonicalize_name from poetry.installation.executor import Executor from poetry.puzzle.transaction import Transaction from poetry.repositories import Repository from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.lockfile_repository import LockfileRepository from poetry.utils.constants import POETRY_SYSTEM_PROJECT_NAME if TYPE_CHECKING: from collections.abc import Iterable from collections.abc import Mapping from cleo.io.io import IO from packaging.utils import NormalizedName from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.path_dependency import PathDependency from poetry.core.packages.project_package import ProjectPackage from poetry.config.config import Config from poetry.installation.operations.operation import Operation from poetry.packages import Locker from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.utils.env import Env class Installer: def __init__( self, io: IO, env: Env, package: ProjectPackage, locker: Locker, pool: RepositoryPool, config: Config, installed: InstalledRepository | None = None, executor: Executor | None = None, disable_cache: bool = False, *, build_constraints: Mapping[NormalizedName, list[Dependency]] | None = None, ) -> None: self._io = io self._env = env self._package = package self._locker = locker self._pool = pool self._config = config self._dry_run = False self._requires_synchronization = False self._update = False self._verbose = False self._groups: Iterable[NormalizedName] | None = None self._skip_directory = False self._lock = False self._whitelist: list[NormalizedName] = [] self._extras: list[NormalizedName] = [] if executor is None: executor = Executor( self._env, self._pool, config, self._io, disable_cache=disable_cache, build_constraints=build_constraints, ) self._executor = executor if installed is None: installed = self._get_installed() self._installed_repository = installed @property def executor(self) -> Executor: return self._executor def set_package(self, package: ProjectPackage) -> Installer: self._package = package return self def set_locker(self, locker: Locker) -> Installer: self._locker = locker return self def run(self) -> int: # Check if refresh if not self._update and self._lock and self._locker.is_locked(): return self._do_refresh() # Force update if there is no lock file present if not self._update and not self._locker.is_locked(): self._update = True if self.is_dry_run(): self.verbose(True) return self._do_install() def dry_run(self, dry_run: bool = True) -> Installer: self._dry_run = dry_run self._executor.dry_run(dry_run) return self def is_dry_run(self) -> bool: return self._dry_run def requires_synchronization( self, requires_synchronization: bool = True ) -> Installer: self._requires_synchronization = requires_synchronization return self def verbose(self, verbose: bool = True) -> Installer: self._verbose = verbose self._executor.verbose(verbose) return self def is_verbose(self) -> bool: return self._verbose def only_groups(self, groups: Iterable[NormalizedName]) -> Installer: self._groups = groups return self def update(self, update: bool = True) -> Installer: self._update = update return self def skip_directory(self, skip_directory: bool = False) -> Installer: self._skip_directory = skip_directory return self def lock(self, update: bool = True) -> Installer: """ Prepare the installer for locking only. """ self.update(update=update) self.execute_operations(False) self._lock = True return self def is_updating(self) -> bool: return self._update def execute_operations(self, execute: bool = True) -> Installer: if not execute: self._executor.disable() return self def whitelist(self, packages: Iterable[str]) -> Installer: self._whitelist = [canonicalize_name(p) for p in packages] return self def extras(self, extras: list[str]) -> Installer: self._extras = [canonicalize_name(extra) for extra in extras] return self def _do_refresh(self) -> int: from poetry.puzzle.solver import Solver # Checking extras for extra in self._extras: if extra not in self._package.extras: raise ValueError(f"Extra [{extra}] is not specified.") locked_repository = self._locker.locked_repository() solver = Solver( self._package, self._pool, locked_repository.packages, locked_repository.packages, self._io, ) # Always re-solve directory dependencies, otherwise we can't determine # if anything has changed (and the lock file contains an invalid version). use_latest = [ p.name for p in locked_repository.packages if p.source_type == "directory" ] with solver.provider.use_source_root( source_root=self._env.path.joinpath("src") ): solved_packages = solver.solve(use_latest=use_latest).get_solved_packages() self._write_lock_file(solved_packages, force=True) return 0 def _do_install(self) -> int: from poetry.puzzle.solver import Solver locked_repository = Repository("poetry-locked") reresolve = self._config.get("installer.re-resolve", False) solved_packages: dict[Package, TransitivePackageInfo] = {} lockfile_repo = LockfileRepository() if self._update: if not self._lock and self._locker.is_locked(): locked_repository = self._locker.locked_repository() # If no packages have been whitelisted (The ones we want to update), # we whitelist every package in the lock file. if not self._whitelist: for pkg in locked_repository.packages: self._whitelist.append(pkg.name) # Checking extras for extra in self._extras: if extra not in self._package.extras: raise ValueError(f"Extra [{extra}] is not specified.") self._io.write_line("Updating dependencies") solver = Solver( self._package, self._pool, self._installed_repository.packages, locked_repository.packages, self._io, ) with solver.provider.use_source_root( source_root=self._env.path.joinpath("src") ): solved_packages = solver.solve( use_latest=self._whitelist ).get_solved_packages() if not self.executor.enabled: # If we are only in lock mode, no need to go any further self._write_lock_file(solved_packages) return 0 for package in solved_packages: if not lockfile_repo.has_package(package): lockfile_repo.add_package(package) else: self._io.write_line("Installing dependencies from lock file") if not self._locker.is_fresh(): raise ValueError( "pyproject.toml changed significantly since poetry.lock was last" f" generated. Run `{self._lock_fix_command()}` to fix the lock file." ) if not (reresolve or self._locker.is_locked_groups_and_markers()): if self._io.is_verbose(): self._io.write_line( "Cannot install without re-resolving" " because the lock file is not at least version 2.1" ) reresolve = True locker_extras = { canonicalize_name(extra) for extra in self._locker.lock_data.get("extras", {}) } for extra in self._extras: if extra not in locker_extras: raise ValueError(f"Extra [{extra}] is not specified.") locked_repository = self._locker.locked_repository() if reresolve: lockfile_repo = locked_repository else: solved_packages = self._locker.locked_packages() if self._io.is_verbose(): self._io.write_line("") self._io.write_line( "Finding the necessary packages for the current system" ) if reresolve: if self._groups is not None: root = self._package.with_dependency_groups( list(self._groups), only=True ) else: root = self._package.without_optional_dependency_groups() # We resolve again by only using the lock file packages = lockfile_repo.packages + locked_repository.packages pool = RepositoryPool.from_packages(packages, self._config) solver = Solver( root, pool, self._installed_repository.packages, locked_repository.packages, NullIO(), active_root_extras=self._extras, ) # Everything is resolved at this point, so we no longer need # to load deferred dependencies (i.e. VCS, URL and path dependencies) solver.provider.load_deferred(False) with solver.use_environment(self._env): transaction = solver.solve(use_latest=self._whitelist) else: if self._groups is None: groups = self._package.dependency_group_names() else: groups = set(self._groups) transaction = Transaction( locked_repository.packages, solved_packages, self._installed_repository.packages, self._package, self._env.marker_env, groups, ) ops = transaction.calculate_operations( with_uninstalls=( self._requires_synchronization or (self._update and not reresolve) ), synchronize=self._requires_synchronization, skip_directory=self._skip_directory, extras=set(self._extras), system_site_packages={ p.name for p in self._installed_repository.system_site_packages }, ) if reresolve and not self._requires_synchronization: # If no packages synchronisation has been requested we need # to calculate the uninstall operations transaction = Transaction( locked_repository.packages, lockfile_repo.packages, installed_packages=self._installed_repository.packages, root_package=root, ) ops = [ op for op in transaction.calculate_operations(with_uninstalls=True) if op.job_type == "uninstall" ] + ops # Validate the dependencies for op in ops: dep = op.package.to_dependency() if dep.is_file() or dep.is_directory(): dep = cast("PathDependency", dep) dep.validate(raise_error=not op.skipped) # Execute operations status = self._execute(ops) if status == 0 and self._update: # Only write lock file when installation is success self._write_lock_file(solved_packages) return status def _lock_fix_command(self) -> str: # `poetry self` commands operate on Poetry's own system project. When the lock # file is outdated, users should run `poetry self lock` rather than `poetry lock`. if self._package.name == POETRY_SYSTEM_PROJECT_NAME: return "poetry self lock" return "poetry lock" def _write_lock_file( self, packages: dict[Package, TransitivePackageInfo], force: bool = False, ) -> None: if not self.is_dry_run() and (force or self._update): updated_lock = self._locker.set_lock_data(self._package, packages) if updated_lock: self._io.write_line("") self._io.write_line("Writing lock file") def _execute(self, operations: list[Operation]) -> int: return self._executor.execute(operations) def _get_installed(self) -> InstalledRepository: return InstalledRepository.load(self._env) ================================================ FILE: src/poetry/installation/operations/__init__.py ================================================ from __future__ import annotations from poetry.installation.operations.install import Install from poetry.installation.operations.uninstall import Uninstall from poetry.installation.operations.update import Update __all__ = ["Install", "Uninstall", "Update"] ================================================ FILE: src/poetry/installation/operations/install.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.installation.operations.operation import Operation if TYPE_CHECKING: from poetry.core.packages.package import Package class Install(Operation): def __init__( self, package: Package, reason: str | None = None, priority: int = 0 ) -> None: super().__init__(reason, priority=priority) self._package = package @property def package(self) -> Package: return self._package @property def job_type(self) -> str: return "install" def __str__(self) -> str: return ( "Installing" f" {self.package.pretty_name} ({self.format_version(self.package)})" ) def __repr__(self) -> str: return ( "" ) ================================================ FILE: src/poetry/installation/operations/operation.py ================================================ from __future__ import annotations from abc import ABC from abc import abstractmethod from typing import TYPE_CHECKING if TYPE_CHECKING: from poetry.core.packages.package import Package from typing_extensions import Self class Operation(ABC): def __init__(self, reason: str | None = None, priority: float = 0) -> None: self._reason = reason self._skipped = False self._skip_reason: str | None = None self._priority = priority @property @abstractmethod def job_type(self) -> str: ... @property def reason(self) -> str | None: return self._reason @property def skipped(self) -> bool: return self._skipped @property def skip_reason(self) -> str | None: return self._skip_reason @property def priority(self) -> float: return self._priority @property @abstractmethod def package(self) -> Package: ... def format_version(self, package: Package) -> str: version: str = package.full_pretty_version return version def skip(self, reason: str) -> Self: self._skipped = True self._skip_reason = reason return self ================================================ FILE: src/poetry/installation/operations/uninstall.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.installation.operations.operation import Operation if TYPE_CHECKING: from poetry.core.packages.package import Package class Uninstall(Operation): def __init__( self, package: Package, reason: str | None = None, priority: float = float("inf"), ) -> None: super().__init__(reason, priority=priority) self._package = package @property def package(self) -> Package: return self._package @property def job_type(self) -> str: return "uninstall" def __str__(self) -> str: return ( "Uninstalling" f" {self.package.pretty_name} ({self.format_version(self._package)})" ) def __repr__(self) -> str: return ( "" ) ================================================ FILE: src/poetry/installation/operations/update.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.installation.operations.operation import Operation if TYPE_CHECKING: from poetry.core.packages.package import Package class Update(Operation): def __init__( self, initial: Package, target: Package, reason: str | None = None, priority: int = 0, ) -> None: self._initial_package = initial self._target_package = target super().__init__(reason, priority=priority) @property def initial_package(self) -> Package: return self._initial_package @property def target_package(self) -> Package: return self._target_package @property def package(self) -> Package: return self._target_package @property def job_type(self) -> str: return "update" def __str__(self) -> str: init_version = self.format_version(self.initial_package) target_version = self.format_version(self.target_package) return ( f"Updating {self.initial_package.pretty_name} ({init_version}) " f"to {self.target_package.pretty_name} ({target_version})" ) def __repr__(self) -> str: init_version = self.format_version(self.initial_package) target_version = self.format_version(self.target_package) return ( f"" ) ================================================ FILE: src/poetry/installation/wheel_installer.py ================================================ from __future__ import annotations import logging import platform import sys from pathlib import Path from typing import TYPE_CHECKING from installer import install from installer.destinations import SchemeDictionaryDestination from installer.sources import WheelFile from installer.sources import _WheelFileValidationError from poetry.__version__ import __version__ from poetry.utils._compat import WINDOWS logger = logging.getLogger(__name__) if TYPE_CHECKING: from collections.abc import Collection from typing import BinaryIO from installer.records import RecordEntry from installer.scripts import LauncherKind from installer.utils import Scheme from poetry.utils.env import Env class WheelDestination(SchemeDictionaryDestination): """ """ def write_to_fs( self, scheme: Scheme, path: str, stream: BinaryIO, is_executable: bool, ) -> RecordEntry: from installer.records import Hash from installer.records import RecordEntry from installer.utils import copyfileobj_with_hashing from installer.utils import make_file_executable target_path = Path(self.scheme_dict[scheme]) / path if target_path.exists(): # Contrary to the base library we don't raise an error here since it can # break pkgutil-style and pkg_resource-style namespace packages. logger.warning(f"Installing {target_path} over existing file") parent_folder = target_path.parent if not parent_folder.exists(): # Due to the parallel installation it can happen # that two threads try to create the directory. parent_folder.mkdir(parents=True, exist_ok=True) with target_path.open("wb") as f: hash_, size = copyfileobj_with_hashing(stream, f, self.hash_algorithm) if is_executable: make_file_executable(target_path) return RecordEntry(path, Hash(self.hash_algorithm, hash_), size) class WheelInstaller: def __init__(self, env: Env) -> None: self._env = env script_kind: LauncherKind if not WINDOWS: script_kind = "posix" else: if platform.uname()[4].startswith("arm"): script_kind = "win-arm64" if sys.maxsize > 2**32 else "win-arm" else: script_kind = "win-amd64" if sys.maxsize > 2**32 else "win-ia32" self._script_kind = script_kind self._bytecode_optimization_levels: Collection[int] = () self.invalid_wheels: dict[Path, list[str]] = {} def enable_bytecode_compilation(self, enable: bool = True) -> None: self._bytecode_optimization_levels = (-1,) if enable else () def install(self, wheel: Path) -> None: with WheelFile.open(wheel) as source: try: # Content validation is temporarily disabled because of # pypa/installer's out of memory issues with big wheels. See # https://github.com/python-poetry/poetry/issues/7983 source.validate_record(validate_contents=False) except _WheelFileValidationError as e: self.invalid_wheels[wheel] = e.issues scheme_dict = self._env.scheme_dict.copy() scheme_dict["headers"] = str( Path(scheme_dict["include"]) / source.distribution ) destination = WheelDestination( scheme_dict, interpreter=str(self._env.python), script_kind=self._script_kind, bytecode_optimization_levels=self._bytecode_optimization_levels, ) install( source=source, destination=destination, # Additional metadata that is generated by the installation tool. additional_metadata={ "INSTALLER": f"Poetry {__version__}".encode(), }, ) ================================================ FILE: src/poetry/json/__init__.py ================================================ from __future__ import annotations import json from importlib.resources import files from typing import Any import fastjsonschema from fastjsonschema.exceptions import JsonSchemaValueException def validate_object(obj: dict[str, Any]) -> list[str]: schema = json.loads( (files(__package__) / "schemas" / "poetry.json").read_text(encoding="utf-8") ) validate = fastjsonschema.compile(schema) errors = [] try: validate(obj) except JsonSchemaValueException as e: errors = [e.message] core_schema = json.loads( (files("poetry.core") / "json" / "schemas" / "poetry-schema.json").read_text( encoding="utf-8" ) ) properties = schema["properties"].keys() | core_schema["properties"].keys() additional_properties = obj.keys() - properties for key in additional_properties: errors.append(f"Additional properties are not allowed ('{key}' was unexpected)") return errors ================================================ FILE: src/poetry/json/schemas/poetry.json ================================================ { "$schema": "http://json-schema.org/draft-04/schema#", "additionalProperties": true, "type": "object", "required": [], "properties": { "requires-poetry": { "type": "string", "description": "The version constraint for Poetry itself.", "$ref": "#/definitions/dependency" }, "requires-plugins": { "type": "object", "description": "Poetry plugins that are required for this project.", "$ref": "#/definitions/dependencies", "additionalProperties": false }, "source": { "type": "array", "description": "A set of additional repositories where packages can be found.", "additionalProperties": { "$ref": "#/definitions/repository" }, "items": { "$ref": "#/definitions/repository" } }, "build-constraints": { "type": "object", "description": "This is a dict of package name (keys) and version constraints (values) to restrict build requirements for a package.", "patternProperties": { "^[a-zA-Z-_.0-9]+$": { "$ref": "#/definitions/dependencies" } } } }, "definitions": { "repository": { "type": "object", "additionalProperties": false, "required": [ "name" ], "properties": { "name": { "type": "string", "description": "The name of the repository." }, "url": { "type": "string", "description": "The url of the repository.", "format": "uri" }, "priority": { "enum": [ "primary", "supplemental", "explicit" ], "description": "Declare the priority of this repository." }, "links": { "type": "boolean", "description": "Declare this as a link source. Links at uri/path can point to sdist or bdist archives." }, "indexed": { "type": "boolean", "description": "For PEP 503 simple API repositories, pre-fetch and index the available packages. (experimental)" } } }, "dependencies": { "type": "object", "patternProperties": { "^[a-zA-Z-_.0-9]+$": { "oneOf": [ { "$ref": "#/definitions/dependency" }, { "$ref": "#/definitions/long-dependency" }, { "$ref": "#/definitions/git-dependency" }, { "$ref": "#/definitions/file-dependency" }, { "$ref": "#/definitions/path-dependency" }, { "$ref": "#/definitions/url-dependency" }, { "$ref": "#/definitions/multiple-constraints-dependency" }, { "$ref": "#/definitions/dependency-options" } ] } } }, "dependency": { "type": "string", "description": "The constraint of the dependency." }, "long-dependency": { "type": "object", "required": [ "version" ], "additionalProperties": false, "properties": { "version": { "type": "string", "description": "The constraint of the dependency." }, "python": { "type": "string", "description": "The python versions for which the dependency should be installed." }, "platform": { "type": "string", "description": "The platform(s) for which the dependency should be installed." }, "markers": { "type": "string", "description": "The PEP 508 compliant environment markers for which the dependency should be installed." }, "allow-prereleases": { "type": "boolean", "description": "Whether the dependency allows prereleases or not." }, "allows-prereleases": { "type": "boolean", "description": "Whether the dependency allows prereleases or not." }, "optional": { "type": "boolean", "description": "Whether the dependency is optional or not." }, "extras": { "type": "array", "description": "The required extras for this dependency.", "items": { "type": "string" } }, "source": { "type": "string", "description": "The exclusive source used to search for this dependency." } } }, "git-dependency": { "type": "object", "required": [ "git" ], "additionalProperties": false, "properties": { "git": { "type": "string", "description": "The url of the git repository." }, "branch": { "type": "string", "description": "The branch to checkout." }, "tag": { "type": "string", "description": "The tag to checkout." }, "rev": { "type": "string", "description": "The revision to checkout." }, "subdirectory": { "type": "string", "description": "The relative path to the directory where the package is located." }, "python": { "type": "string", "description": "The python versions for which the dependency should be installed." }, "platform": { "type": "string", "description": "The platform(s) for which the dependency should be installed." }, "markers": { "type": "string", "description": "The PEP 508 compliant environment markers for which the dependency should be installed." }, "allow-prereleases": { "type": "boolean", "description": "Whether the dependency allows prereleases or not." }, "allows-prereleases": { "type": "boolean", "description": "Whether the dependency allows prereleases or not." }, "optional": { "type": "boolean", "description": "Whether the dependency is optional or not." }, "extras": { "type": "array", "description": "The required extras for this dependency.", "items": { "type": "string" } }, "develop": { "type": "boolean", "description": "Whether to install the dependency in development mode." } } }, "file-dependency": { "type": "object", "required": [ "file" ], "additionalProperties": false, "properties": { "file": { "type": "string", "description": "The path to the file." }, "subdirectory": { "type": "string", "description": "The relative path to the directory where the package is located." }, "python": { "type": "string", "description": "The python versions for which the dependency should be installed." }, "platform": { "type": "string", "description": "The platform(s) for which the dependency should be installed." }, "markers": { "type": "string", "description": "The PEP 508 compliant environment markers for which the dependency should be installed." }, "optional": { "type": "boolean", "description": "Whether the dependency is optional or not." }, "extras": { "type": "array", "description": "The required extras for this dependency.", "items": { "type": "string" } } } }, "path-dependency": { "type": "object", "required": [ "path" ], "additionalProperties": false, "properties": { "path": { "type": "string", "description": "The path to the dependency." }, "subdirectory": { "type": "string", "description": "The relative path to the directory where the package is located." }, "python": { "type": "string", "description": "The python versions for which the dependency should be installed." }, "platform": { "type": "string", "description": "The platform(s) for which the dependency should be installed." }, "markers": { "type": "string", "description": "The PEP 508 compliant environment markers for which the dependency should be installed." }, "optional": { "type": "boolean", "description": "Whether the dependency is optional or not." }, "extras": { "type": "array", "description": "The required extras for this dependency.", "items": { "type": "string" } }, "develop": { "type": "boolean", "description": "Whether to install the dependency in development mode." } } }, "url-dependency": { "type": "object", "required": [ "url" ], "additionalProperties": false, "properties": { "url": { "type": "string", "description": "The url to the file." }, "subdirectory": { "type": "string", "description": "The relative path to the directory where the package is located." }, "python": { "type": "string", "description": "The python versions for which the dependency should be installed." }, "platform": { "type": "string", "description": "The platform(s) for which the dependency should be installed." }, "markers": { "type": "string", "description": "The PEP 508 compliant environment markers for which the dependency should be installed." }, "optional": { "type": "boolean", "description": "Whether the dependency is optional or not." }, "extras": { "type": "array", "description": "The required extras for this dependency.", "items": { "type": "string" } } } }, "dependency-options": { "type": "object", "additionalProperties": false, "properties": { "python": { "type": "string", "description": "The python versions for which the dependency should be installed." }, "platform": { "type": "string", "description": "The platform(s) for which the dependency should be installed." }, "markers": { "type": "string", "description": "The PEP 508 compliant environment markers for which the dependency should be installed." }, "allow-prereleases": { "type": "boolean", "description": "Whether the dependency allows prereleases or not." }, "source": { "type": "string", "description": "The exclusive source used to search for this dependency." }, "develop": { "type": "boolean", "description": "Whether to install the dependency in development mode." } } }, "multiple-constraints-dependency": { "type": "array", "minItems": 1, "items": { "oneOf": [ { "$ref": "#/definitions/dependency" }, { "$ref": "#/definitions/long-dependency" }, { "$ref": "#/definitions/git-dependency" }, { "$ref": "#/definitions/file-dependency" }, { "$ref": "#/definitions/path-dependency" }, { "$ref": "#/definitions/url-dependency" }, { "$ref": "#/definitions/dependency-options" } ] } } } } ================================================ FILE: src/poetry/layouts/__init__.py ================================================ from __future__ import annotations from poetry.layouts.layout import Layout from poetry.layouts.src import SrcLayout _LAYOUTS = {"src": SrcLayout, "standard": Layout} def layout(name: str) -> type[Layout]: if name not in _LAYOUTS: raise ValueError("Invalid layout") return _LAYOUTS[name] ================================================ FILE: src/poetry/layouts/layout.py ================================================ from __future__ import annotations import importlib.metadata from pathlib import Path from typing import TYPE_CHECKING from typing import Any from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.utils.helpers import module_name from poetry.core.utils.patterns import AUTHOR_REGEX from tomlkit import inline_table from tomlkit import loads from tomlkit import table from tomlkit.toml_document import TOMLDocument from poetry.factory import Factory from poetry.pyproject.toml import PyProjectTOML if TYPE_CHECKING: from collections.abc import Mapping from tomlkit.items import InlineTable POETRY_DEFAULT = """\ [project] name = "" version = "" description = "" authors = [ ] license = {} readme = "" requires-python = "" dependencies = [ ] [dependency-groups] dev = [ ] [tool.poetry] packages = [] """ poetry_core_version = Version.parse(importlib.metadata.version("poetry-core")) BUILD_SYSTEM_MIN_VERSION: str | None = Version.from_parts( major=poetry_core_version.major, minor=poetry_core_version.minor if poetry_core_version.major == 0 else 0, patch=poetry_core_version.patch if (poetry_core_version.major, poetry_core_version.minor) == (0, 0) else 0, ).to_string() BUILD_SYSTEM_MAX_VERSION: str | None = poetry_core_version.next_breaking().to_string() class Layout: def __init__( self, project: str, version: str = "0.1.0", description: str = "", readme_format: str = "md", author: str | None = None, license: str | None = None, python: str | None = None, dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, dev_dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, ) -> None: self._project = canonicalize_name(project) self._package_path_relative = Path( *(module_name(part) for part in project.split(".")) ) self._package_name = ".".join(self._package_path_relative.parts) self._version = version self._description = description self._readme_format = readme_format.lower() self._license = license self._python = python self._dependencies = dependencies or {} self._dev_dependencies = dev_dependencies or {} if not author: author = "Your Name " self._author = author @property def basedir(self) -> Path: return Path() @property def package_path(self) -> Path: return self.basedir / self._package_path_relative def get_package_include(self) -> InlineTable | None: package = inline_table() # If a project is created in the root directory (this is reasonable inside a # docker container, eg ) # then parts will be empty. parts = self._package_path_relative.parts if not parts: return None include = parts[0] package.append("include", include) if self.basedir != Path(): package.append("from", self.basedir.as_posix()) else: if module_name(self._project) == include: # package include and package name are the same, # packages table is redundant here. return None return package def create( self, path: Path, with_tests: bool = True, with_pyproject: bool = True ) -> None: path.mkdir(parents=True, exist_ok=True) self._create_default(path) self._create_readme(path) if with_tests: self._create_tests(path) if with_pyproject: self._write_poetry(path) def generate_project_content( self, project_path: Path | None = None ) -> TOMLDocument: template = POETRY_DEFAULT content: dict[str, Any] = loads(template) project_content = content["project"] project_content["name"] = self._project project_content["version"] = self._version project_content["description"] = self._description m = AUTHOR_REGEX.match(self._author) if m is None: # This should not happen because author has been validated before. raise ValueError(f"Invalid author: {self._author}") else: author = {"name": m.group("name")} if email := m.group("email"): author["email"] = email project_content["authors"].append(author) if self._license: project_content["license"]["text"] = self._license else: project_content.remove("license") if project_path: project_dir = project_path / f"README.{self._readme_format}" abs_path = project_dir.resolve() if abs_path.exists(): project_content["readme"] = f"README.{self._readme_format}" else: project_content.remove("readme") if self._python: project_content["requires-python"] = self._python else: project_content.remove("requires-python") for dep_name, dep_constraint in self._dependencies.items(): dependency = Factory.create_dependency(dep_name, dep_constraint) project_content["dependencies"].append(dependency.to_pep_508()) poetry_content = content["tool"]["poetry"] packages = self.get_package_include() if packages: poetry_content["packages"].append(packages) else: poetry_content.remove("packages") if self._dev_dependencies: for dep_name, dep_constraint in self._dev_dependencies.items(): dependency = Factory.create_dependency(dep_name, dep_constraint) content["dependency-groups"]["dev"].append(dependency.to_pep_508()) else: del content["dependency-groups"] if not poetry_content: del content["tool"]["poetry"] # Add build system build_system = table() build_system_version = "" if BUILD_SYSTEM_MIN_VERSION is not None: build_system_version = ">=" + BUILD_SYSTEM_MIN_VERSION if BUILD_SYSTEM_MAX_VERSION is not None: if build_system_version: build_system_version += "," build_system_version += "<" + BUILD_SYSTEM_MAX_VERSION build_system.add("requires", ["poetry-core" + build_system_version]) build_system.add("build-backend", "poetry.core.masonry.api") assert isinstance(content, TOMLDocument) content.add("build-system", build_system) return content def _create_default(self, path: Path, src: bool = True) -> None: package_path = path / self.package_path package_path.mkdir(parents=True) package_init = package_path / "__init__.py" package_init.touch() def _create_readme(self, path: Path) -> Path: readme_file = path.joinpath(f"README.{self._readme_format}") readme_file.touch() return readme_file @staticmethod def _create_tests(path: Path) -> None: tests = path / "tests" tests.mkdir() tests_init = tests / "__init__.py" tests_init.touch(exist_ok=False) def _write_poetry(self, path: Path) -> None: pyproject = PyProjectTOML(path / "pyproject.toml") content = self.generate_project_content() for section, item in content.items(): pyproject.data.append(section, item) pyproject.save() ================================================ FILE: src/poetry/layouts/src.py ================================================ from __future__ import annotations from pathlib import Path from poetry.layouts.layout import Layout class SrcLayout(Layout): @property def basedir(self) -> Path: return Path("src") ================================================ FILE: src/poetry/locations.py ================================================ from __future__ import annotations import os from pathlib import Path from platformdirs import user_cache_path from platformdirs import user_config_path from platformdirs import user_data_path _APP_NAME = "pypoetry" DEFAULT_CACHE_DIR = user_cache_path(_APP_NAME, appauthor=False) CONFIG_DIR = Path( os.getenv("POETRY_CONFIG_DIR") or user_config_path(_APP_NAME, appauthor=False, roaming=True) ) def data_dir() -> Path: if poetry_home := os.getenv("POETRY_HOME"): return Path(poetry_home).expanduser() return user_data_path(_APP_NAME, appauthor=False, roaming=True) ================================================ FILE: src/poetry/masonry/__init__.py ================================================ ================================================ FILE: src/poetry/masonry/api.py ================================================ from __future__ import annotations from poetry.core.masonry.api import build_sdist from poetry.core.masonry.api import build_wheel from poetry.core.masonry.api import get_requires_for_build_sdist from poetry.core.masonry.api import get_requires_for_build_wheel from poetry.core.masonry.api import prepare_metadata_for_build_wheel __all__ = [ "build_sdist", "build_wheel", "get_requires_for_build_sdist", "get_requires_for_build_wheel", "prepare_metadata_for_build_wheel", ] ================================================ FILE: src/poetry/masonry/builders/__init__.py ================================================ from __future__ import annotations from poetry.core.masonry.builders.sdist import SdistBuilder from poetry.core.masonry.builders.wheel import WheelBuilder from poetry.masonry.builders.editable import EditableBuilder __all__ = ["BUILD_FORMATS", "EditableBuilder"] # might be extended by plugins BUILD_FORMATS = { "sdist": SdistBuilder, "wheel": WheelBuilder, } ================================================ FILE: src/poetry/masonry/builders/editable.py ================================================ from __future__ import annotations import csv import hashlib import json import os from base64 import urlsafe_b64encode from pathlib import Path from typing import TYPE_CHECKING from poetry.core.masonry.builders.builder import Builder from poetry.core.masonry.builders.sdist import SdistBuilder from poetry.core.masonry.utils.package_include import PackageInclude from poetry.utils._compat import WINDOWS from poetry.utils._compat import decode from poetry.utils._compat import getencoding from poetry.utils.env import build_environment from poetry.utils.helpers import is_dir_writable from poetry.utils.pip import pip_install if TYPE_CHECKING: from cleo.io.io import IO from poetry.poetry import Poetry from poetry.utils.env import Env SCRIPT_TEMPLATE = """\ #!{python} import sys from {module} import {callable_holder} if __name__ == '__main__': sys.exit({callable_}()) """ WINDOWS_CMD_TEMPLATE = """\ @echo off\r\n"{python}" "%~dp0\\{script}" %*\r\n """ class EditableBuilder(Builder): def __init__(self, poetry: Poetry, env: Env, io: IO) -> None: self._poetry: Poetry super().__init__(poetry) self._env = env self._io = io def build(self, target_dir: Path | None = None) -> Path: self._debug( f" - Building package {self._package.name} in" " editable mode" ) if self._package.build_script: if self._package.build_should_generate_setup(): self._debug( " - Falling back on using a setup.py" ) self._setup_build() return self._path self._run_build_script(self._package.build_script) for removed in self._env.site_packages.remove_distribution_files( distribution_name=self._package.name ): self._debug( f" - Removed {removed.name} directory from" f" {removed.parent}" ) added_files = [] added_files += self._add_pth() added_files += self._add_scripts() self._add_dist_info(added_files) return self._path def _run_build_script(self, build_script: str) -> None: with build_environment(poetry=self._poetry, env=self._env, io=self._io) as env: self._debug(f" - Executing build script: {build_script}") env.run("python", str(self._path.joinpath(build_script)), call=True) def _setup_build(self) -> None: builder = SdistBuilder(self._poetry) setup = self._path / "setup.py" has_setup = setup.exists() if has_setup: self._io.write_error_line( "A setup.py file already exists. Using it." ) else: with setup.open("w", encoding="utf-8") as f: f.write(decode(builder.build_setup())) try: pip_install(self._path, self._env, upgrade=True, editable=True) finally: if not has_setup: os.remove(setup) def _add_pth(self) -> list[Path]: paths = { include.base.resolve().as_posix() for include in self._module.includes if isinstance(include, PackageInclude) and (include.is_module() or include.is_package()) } content = "".join(decode(path + os.linesep) for path in paths) pth_file = Path(self._module.name).with_suffix(".pth") # remove any pre-existing pth files for this package for file in self._env.site_packages.find(path=pth_file, writable_only=True): self._debug( f" - Removing existing {file.name} from {file.parent}" f" for {self._poetry.file.path.parent}" ) file.unlink(missing_ok=True) try: pth_file = self._env.site_packages.write_text( pth_file, content, encoding=getencoding() ) self._debug( f" - Adding {pth_file.name} to {pth_file.parent} for" f" {self._poetry.file.path.parent}" ) return [pth_file] except PermissionError: self._io.write_error_line( f" - Failed to create {pth_file.name} for" f" {self._poetry.file.path.parent}" ) return [] def _add_scripts(self) -> list[Path]: added = [] entry_points = self.convert_entry_points() for scripts_path in self._env.script_dirs: if is_dir_writable(path=scripts_path, create=True): break else: self._io.write_error_line( " - Failed to find a suitable script installation directory for" f" {self._poetry.file.path.parent}" ) return [] scripts = entry_points.get("console_scripts", []) for script in scripts: name, script_with_extras = script.split(" = ") script_without_extras = script_with_extras.split("[")[0] try: module, callable_ = script_without_extras.split(":") except ValueError as exc: msg = ( f"Bad script ({name}): script needs to specify a function within a" " module like: module(.submodule):function\nInstead got:" f" {script_with_extras}" ) if "not enough values" in str(exc): msg += ( "\nHint: If the script depends on module-level code, try" " wrapping it in a main() function and modifying your script" f' like:\n{name} = "{script_with_extras}:main"' ) elif "too many values" in str(exc): msg += '\nToo many ":" found!' raise ValueError(msg) callable_holder = callable_.split(".", 1)[0] script_file = scripts_path.joinpath(name) self._debug( f" - Adding the {name} script to {scripts_path}" ) with script_file.open("w", encoding="utf-8") as f: f.write( decode( SCRIPT_TEMPLATE.format( python=self._env.python, module=module, callable_holder=callable_holder, callable_=callable_, ) ) ) script_file.chmod(0o755) added.append(script_file) if WINDOWS: cmd_script = script_file.with_suffix(".cmd") cmd = WINDOWS_CMD_TEMPLATE.format(python=self._env.python, script=name) self._debug( f" - Adding the {cmd_script.name} script wrapper to" f" {scripts_path}" ) with cmd_script.open("w", encoding="utf-8") as f: f.write(decode(cmd)) added.append(cmd_script) return added def _add_dist_info(self, added_files: list[Path]) -> None: from poetry.core.masonry.builders.wheel import WheelBuilder builder = WheelBuilder(self._poetry) dist_info = self._env.site_packages.mkdir(Path(builder.dist_info)) self._debug( f" - Adding the {dist_info.name} directory to" f" {dist_info.parent}" ) builder.prepare_metadata(dist_info.parent) for path in sorted(f for f in dist_info.rglob("*") if f.is_file()): added_files.append(path) with dist_info.joinpath("INSTALLER").open("w", encoding="utf-8") as f: f.write("poetry") added_files.append(dist_info.joinpath("INSTALLER")) # write PEP 610 metadata direct_url_json = dist_info.joinpath("direct_url.json") direct_url_json.write_text( json.dumps( { "dir_info": {"editable": True}, "url": self._poetry.file.path.parent.absolute().as_uri(), } ), encoding="utf-8", ) added_files.append(direct_url_json) record = dist_info.joinpath("RECORD") with record.open("w", encoding="utf-8", newline="") as f: csv_writer = csv.writer(f) for path in added_files: hash = self._get_file_hash(path) size = path.stat().st_size csv_writer.writerow((path, f"sha256={hash}", size)) # RECORD itself is recorded with no hash or size csv_writer.writerow((record, "", "")) def _get_file_hash(self, filepath: Path) -> str: hashsum = hashlib.sha256() with filepath.open("rb") as src: while True: buf = src.read(1024 * 8) if not buf: break hashsum.update(buf) src.seek(0) return urlsafe_b64encode(hashsum.digest()).decode("ascii").rstrip("=") def _debug(self, msg: str) -> None: if self._io.is_debug(): self._io.write_line(msg) ================================================ FILE: src/poetry/mixology/__init__.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.mixology.version_solver import VersionSolver if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage from poetry.mixology.result import SolverResult from poetry.puzzle.provider import Provider def resolve_version(root: ProjectPackage, provider: Provider) -> SolverResult: solver = VersionSolver(root, provider) return solver.solve() ================================================ FILE: src/poetry/mixology/assignment.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.mixology.term import Term if TYPE_CHECKING: from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.mixology.incompatibility import Incompatibility class Assignment(Term): """ A term in a PartialSolution that tracks some additional metadata. """ def __init__( self, dependency: Dependency, is_positive: bool, decision_level: int, index: int, cause: Incompatibility | None = None, ) -> None: super().__init__(dependency, is_positive) self._decision_level = decision_level self._index = index self._cause = cause @property def decision_level(self) -> int: return self._decision_level @property def index(self) -> int: return self._index @property def cause(self) -> Incompatibility | None: return self._cause @classmethod def decision(cls, package: Package, decision_level: int, index: int) -> Assignment: return cls(package.to_dependency(), True, decision_level, index) @classmethod def derivation( cls, dependency: Dependency, is_positive: bool, cause: Incompatibility, decision_level: int, index: int, ) -> Assignment: return cls(dependency, is_positive, decision_level, index, cause) def is_decision(self) -> bool: return self._cause is None ================================================ FILE: src/poetry/mixology/failure.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.core.constraints.version import parse_constraint from poetry.mixology.incompatibility_cause import ConflictCauseError from poetry.mixology.incompatibility_cause import PythonCauseError if TYPE_CHECKING: from poetry.mixology.incompatibility import Incompatibility class SolveFailureError(Exception): def __init__(self, incompatibility: Incompatibility) -> None: self._incompatibility = incompatibility @property def message(self) -> str: return str(self) def __str__(self) -> str: return _Writer(self._incompatibility).write() class _Writer: def __init__(self, root: Incompatibility) -> None: self._root = root self._derivations: dict[Incompatibility, int] = {} self._lines: list[tuple[str, int | None]] = [] self._line_numbers: dict[Incompatibility, int] = {} self._count_derivations(self._root) def write(self) -> str: buffer = [] version_solutions = [] required_python_version_notification = False for incompatibility in self._root.external_incompatibilities: if isinstance(incompatibility.cause, PythonCauseError): root_constraint = parse_constraint( incompatibility.cause.root_python_version ) constraint = parse_constraint(incompatibility.cause.python_version) version_solutions.append( "For " f"{incompatibility.terms[0].dependency.name}," " a possible solution would be to set the" " `python` property to" f' "{root_constraint.intersect(constraint)}"' ) if not required_python_version_notification: buffer.append( "The current project's supported Python range" f" ({incompatibility.cause.root_python_version}) is not" " compatible with some of the required packages Python" " requirement:" ) required_python_version_notification = True root_constraint = parse_constraint( incompatibility.cause.root_python_version ) constraint = parse_constraint(incompatibility.cause.python_version) buffer.append( f" - {incompatibility.terms[0].dependency.name} requires Python" f" {incompatibility.cause.python_version}, so it will not be" f" installable for Python {root_constraint.difference(constraint)}" ) if required_python_version_notification: buffer.append("") if isinstance(self._root.cause, ConflictCauseError): self._visit(self._root) else: self._write(self._root, f"Because {self._root}, version solving failed.") padding = ( 0 if not self._line_numbers else len(f"({list(self._line_numbers.values())[-1]}) ") ) last_was_empty = False for line in self._lines: message = line[0] if not message: if not last_was_empty: buffer.append("") last_was_empty = True continue last_was_empty = False number = line[-1] if number is not None: message = f"({number})".ljust(padding) + message else: message = " " * padding + message buffer.append(message) if required_python_version_notification: # Add suggested solution links = ",".join( f"\n https://python-poetry.org/docs/dependency-specification/#{section}" for section in [ "python-restricted-dependencies", "using-environment-markers", ] ) description = ( "The Python requirement can be specified via the" " `python` or" " `markers` properties" ) if version_solutions: description += "\n\n " + "\n".join(version_solutions) description = description.strip(" ") buffer.append( f"\n * " f"Check your dependencies Python requirement:" f" {description}\n{links}\n", ) return "\n".join(buffer) def _write( self, incompatibility: Incompatibility, message: str, numbered: bool = False ) -> None: if numbered: number = len(self._line_numbers) + 1 self._line_numbers[incompatibility] = number self._lines.append((message, number)) else: self._lines.append((message, None)) def _visit( self, incompatibility: Incompatibility, conclusion: bool = False, ) -> None: numbered = conclusion or self._derivations[incompatibility] > 1 conjunction = "So," if conclusion or incompatibility == self._root else "And" incompatibility_string = str(incompatibility) cause = incompatibility.cause assert isinstance(cause, ConflictCauseError) if isinstance(cause.conflict.cause, ConflictCauseError) and isinstance( cause.other.cause, ConflictCauseError ): conflict_line = self._line_numbers.get(cause.conflict) other_line = self._line_numbers.get(cause.other) if conflict_line is not None and other_line is not None: reason = cause.conflict.and_to_string( cause.other, conflict_line, other_line ) self._write( incompatibility, f"Because {reason}, {incompatibility_string}.", numbered=numbered, ) elif conflict_line is not None or other_line is not None: if conflict_line is not None: with_line = cause.conflict without_line = cause.other line = conflict_line elif other_line is not None: with_line = cause.other without_line = cause.conflict line = other_line self._visit(without_line) self._write( incompatibility, f"{conjunction} because {with_line!s} ({line})," f" {incompatibility_string}.", numbered=numbered, ) else: single_line_conflict = self._is_single_line(cause.conflict.cause) single_line_other = self._is_single_line(cause.other.cause) if single_line_other or single_line_conflict: first = cause.conflict if single_line_other else cause.other second = cause.other if single_line_other else cause.conflict self._visit(first) self._visit(second) self._write( incompatibility, f"Thus, {incompatibility_string}.", numbered=numbered, ) else: self._visit(cause.conflict, conclusion=True) self._lines.append(("", None)) self._visit(cause.other) self._write( incompatibility, f"{conjunction} because {cause.conflict!s}" f" ({self._line_numbers[cause.conflict]})," f" {incompatibility_string}", numbered=numbered, ) elif isinstance(cause.conflict.cause, ConflictCauseError) or isinstance( cause.other.cause, ConflictCauseError ): derived = ( cause.conflict if isinstance(cause.conflict.cause, ConflictCauseError) else cause.other ) ext = ( cause.other if isinstance(cause.conflict.cause, ConflictCauseError) else cause.conflict ) derived_line = self._line_numbers.get(derived) if derived_line is not None: reason = ext.and_to_string(derived, None, derived_line) self._write( incompatibility, f"Because {reason}, {incompatibility_string}.", numbered=numbered, ) elif self._is_collapsible(derived): derived_cause = derived.cause assert isinstance(derived_cause, ConflictCauseError) if isinstance(derived_cause.conflict.cause, ConflictCauseError): collapsed_derived = derived_cause.conflict collapsed_ext = derived_cause.other else: collapsed_derived = derived_cause.other collapsed_ext = derived_cause.conflict self._visit(collapsed_derived) reason = collapsed_ext.and_to_string(ext, None, None) self._write( incompatibility, f"{conjunction} because {reason}, {incompatibility_string}.", numbered=numbered, ) else: self._visit(derived) self._write( incompatibility, f"{conjunction} because {ext!s}, {incompatibility_string}.", numbered=numbered, ) else: reason = cause.conflict.and_to_string(cause.other, None, None) self._write( incompatibility, f"Because {reason}, {incompatibility_string}.", numbered=numbered, ) def _is_collapsible(self, incompatibility: Incompatibility) -> bool: if self._derivations[incompatibility] > 1: return False cause = incompatibility.cause assert isinstance(cause, ConflictCauseError) if isinstance(cause.conflict.cause, ConflictCauseError) and isinstance( cause.other.cause, ConflictCauseError ): return False if not isinstance(cause.conflict.cause, ConflictCauseError) and not isinstance( cause.other.cause, ConflictCauseError ): return False complex = ( cause.conflict if isinstance(cause.conflict.cause, ConflictCauseError) else cause.other ) return complex not in self._line_numbers def _is_single_line(self, cause: ConflictCauseError) -> bool: return not isinstance( cause.conflict.cause, ConflictCauseError ) and not isinstance(cause.other.cause, ConflictCauseError) def _count_derivations(self, incompatibility: Incompatibility) -> None: if incompatibility in self._derivations: self._derivations[incompatibility] += 1 else: self._derivations[incompatibility] = 1 cause = incompatibility.cause if isinstance(cause, ConflictCauseError): self._count_derivations(cause.conflict) self._count_derivations(cause.other) ================================================ FILE: src/poetry/mixology/incompatibility.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.mixology.incompatibility_cause import ConflictCauseError from poetry.mixology.incompatibility_cause import DependencyCauseError from poetry.mixology.incompatibility_cause import NoVersionsCauseError from poetry.mixology.incompatibility_cause import PlatformCauseError from poetry.mixology.incompatibility_cause import PythonCauseError from poetry.mixology.incompatibility_cause import RootCauseError if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterator from poetry.mixology.incompatibility_cause import IncompatibilityCauseError from poetry.mixology.term import Term class Incompatibility: def __init__(self, terms: list[Term], cause: IncompatibilityCauseError) -> None: # Remove the root package from generated incompatibilities, since it will # always be satisfied. This makes error reporting clearer, and may also # make solving more efficient. if ( len(terms) != 1 and isinstance(cause, ConflictCauseError) and any(term.is_positive() and term.dependency.is_root for term in terms) ): terms = [ term for term in terms if not term.is_positive() or not term.dependency.is_root ] if len(terms) != 1 and ( # Short-circuit in the common case of a two-term incompatibility with # two different packages (for example, a dependency). len(terms) != 2 or terms[0].dependency.complete_name == terms[-1].dependency.complete_name ): # Coalesce multiple terms about the same package if possible. by_name: dict[str, dict[str, Term]] = {} for term in terms: by_ref = by_name.setdefault(term.dependency.complete_name, {}) ref = term.dependency.complete_name if ref in by_ref: value = by_ref[ref].intersect(term) # If we have two terms that refer to the same package but have a # null intersection, they're mutually exclusive, making this # incompatibility irrelevant, since we already know that mutually # exclusive version ranges are incompatible. We should never derive # an irrelevant incompatibility. err_msg = f"Package '{ref}' is listed as a dependency of itself." assert value is not None, err_msg by_ref[ref] = value else: by_ref[ref] = term new_terms = [] for by_ref in by_name.values(): positive_terms = [ term for term in by_ref.values() if term.is_positive() ] if positive_terms: new_terms += positive_terms continue new_terms += list(by_ref.values()) terms = new_terms self._terms = terms self._cause = cause @property def terms(self) -> list[Term]: return self._terms @property def cause(self) -> IncompatibilityCauseError: return self._cause @property def external_incompatibilities( self, ) -> Iterator[Incompatibility]: """ Returns all external incompatibilities in this incompatibility's derivation graph. """ if isinstance(self._cause, ConflictCauseError): cause: ConflictCauseError = self._cause yield from cause.conflict.external_incompatibilities yield from cause.other.external_incompatibilities else: yield self def is_failure(self) -> bool: return len(self._terms) == 0 or ( len(self._terms) == 1 and self._terms[0].dependency.is_root ) def __str__(self) -> str: if isinstance(self._cause, DependencyCauseError): assert len(self._terms) == 2 depender = self._terms[0] dependee = self._terms[1] assert depender.is_positive() assert not dependee.is_positive() return ( f"{self._terse(depender, allow_every=True)} depends on" f" {self._terse(dependee)}" ) elif isinstance(self._cause, PythonCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() text = f"{self._terse(self._terms[0], allow_every=True)} requires " text += f"Python {self._cause.python_version}" return text elif isinstance(self._cause, PlatformCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() text = f"{self._terse(self._terms[0], allow_every=True)} requires " text += f"platform {self._cause.platform}" return text elif isinstance(self._cause, NoVersionsCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() return ( f"no versions of {self._terms[0].dependency.name} match" f" {self._terms[0].constraint}" ) elif isinstance(self._cause, RootCauseError): assert len(self._terms) == 1 assert not self._terms[0].is_positive() assert self._terms[0].dependency.is_root return ( f"{self._terms[0].dependency.name} is" f" {self._terms[0].dependency.constraint}" ) elif self.is_failure(): return "version solving failed" if len(self._terms) == 1: term = self._terms[0] verb = "forbidden" if term.is_positive() else "required" return f"{term.dependency.name} is {verb}" if len(self._terms) == 2: term1 = self._terms[0] term2 = self._terms[1] if term1.is_positive() == term2.is_positive(): if not term1.is_positive(): return f"either {self._terse(term1)} or {self._terse(term2)}" package1 = ( term1.dependency.name if term1.constraint.is_any() else self._terse(term1) ) package2 = ( term2.dependency.name if term2.constraint.is_any() else self._terse(term2) ) return f"{package1} is incompatible with {package2}" positive = [] negative = [] for term in self._terms: if term.is_positive(): positive.append(self._terse(term)) else: negative.append(self._terse(term)) if positive and negative: if len(positive) != 1: return f"if {' and '.join(positive)} then {' or '.join(negative)}" positive_term = next(term for term in self._terms if term.is_positive()) return ( f"{self._terse(positive_term, allow_every=True)} requires" f" {' or '.join(negative)}" ) elif positive: return f"one of {' or '.join(positive)} must be false" else: return f"one of {' or '.join(negative)} must be true" def and_to_string( self, other: Incompatibility, this_line: int | None, other_line: int | None, ) -> str: requires_both = self._try_requires_both(other, this_line, other_line) if requires_both is not None: return requires_both requires_through = self._try_requires_through(other, this_line, other_line) if requires_through is not None: return requires_through requires_forbidden = self._try_requires_forbidden(other, this_line, other_line) if requires_forbidden is not None: return requires_forbidden buffer = [str(self)] if this_line is not None: buffer.append(f" {this_line!s}") buffer.append(f" and {other!s}") if other_line is not None: buffer.append(f" {other_line!s}") return "\n".join(buffer) def _try_requires_both( self, other: Incompatibility, this_line: int | None, other_line: int | None, ) -> str | None: if len(self._terms) == 1 or len(other.terms) == 1: return None this_positive = self._single_term_where(lambda term: term.is_positive()) if this_positive is None: return None other_positive = other._single_term_where(lambda term: term.is_positive()) if other_positive is None: return None if this_positive.dependency != other_positive.dependency: return None this_negatives = " or ".join( [self._terse(term) for term in self._terms if not term.is_positive()] ) other_negatives = " or ".join( [self._terse(term) for term in other.terms if not term.is_positive()] ) buffer = [self._terse(this_positive, allow_every=True) + " "] is_dependency = isinstance(self.cause, DependencyCauseError) and isinstance( other.cause, DependencyCauseError ) if is_dependency: buffer.append("depends on") else: buffer.append("requires") buffer.append(f" both {this_negatives}") if this_line is not None: buffer.append(f" ({this_line})") buffer.append(f" and {other_negatives}") if other_line is not None: buffer.append(f" ({other_line})") return "".join(buffer) def _try_requires_through( self, other: Incompatibility, this_line: int | None, other_line: int | None, ) -> str | None: if len(self._terms) == 1 or len(other.terms) == 1: return None this_negative = self._single_term_where(lambda term: not term.is_positive()) other_negative = other._single_term_where(lambda term: not term.is_positive()) if this_negative is None and other_negative is None: return None this_positive = self._single_term_where(lambda term: term.is_positive()) other_positive = self._single_term_where(lambda term: term.is_positive()) if ( this_negative is not None and other_positive is not None and this_negative.dependency.name == other_positive.dependency.name and this_negative.inverse.satisfies(other_positive) ): prior = self prior_negative = this_negative prior_line = this_line latter = other latter_line = other_line elif ( other_negative is not None and this_positive is not None and other_negative.dependency.name == this_positive.dependency.name and other_negative.inverse.satisfies(this_positive) ): prior = other prior_negative = other_negative prior_line = other_line latter = self latter_line = this_line else: return None prior_positives = [term for term in prior.terms if term.is_positive()] buffer = [] if len(prior_positives) > 1: prior_string = " or ".join([self._terse(term) for term in prior_positives]) buffer.append(f"if {prior_string} then ") else: if isinstance(prior.cause, DependencyCauseError): verb = "depends on" else: verb = "requires" buffer.append( f"{self._terse(prior_positives[0], allow_every=True)} {verb} " ) buffer.append(self._terse(prior_negative)) if prior_line is not None: buffer.append(f" ({prior_line})") buffer.append(" which ") if isinstance(latter.cause, DependencyCauseError): buffer.append("depends on ") else: buffer.append("requires ") buffer.append( " or ".join( [self._terse(term) for term in latter.terms if not term.is_positive()] ) ) if latter_line is not None: buffer.append(f" ({latter_line})") return "".join(buffer) def _try_requires_forbidden( self, other: Incompatibility, this_line: int | None, other_line: int | None, ) -> str | None: if len(self._terms) != 1 and len(other.terms) != 1: return None if len(self.terms) == 1: prior = other latter = self prior_line = other_line latter_line = this_line else: prior = self latter = other prior_line = this_line latter_line = other_line negative = prior._single_term_where(lambda term: not term.is_positive()) if negative is None: return None if not negative.inverse.satisfies(latter.terms[0]): return None positives = [t for t in prior.terms if t.is_positive()] buffer = [] if len(positives) > 1: prior_string = " or ".join([self._terse(term) for term in positives]) buffer.append(f"if {prior_string} then ") else: buffer.append(self._terse(positives[0], allow_every=True)) if isinstance(prior.cause, DependencyCauseError): buffer.append(" depends on ") else: buffer.append(" requires ") buffer.append(self._terse(latter.terms[0]) + " ") if prior_line is not None: buffer.append(f"({prior_line}) ") if isinstance(latter.cause, PythonCauseError): cause: PythonCauseError = latter.cause buffer.append(f"which requires Python {cause.python_version}") elif isinstance(latter.cause, NoVersionsCauseError): buffer.append("which doesn't match any versions") else: buffer.append("which is forbidden") if latter_line is not None: buffer.append(f" ({latter_line})") return "".join(buffer) def _terse(self, term: Term, allow_every: bool = False) -> str: if allow_every and term.constraint.is_any(): return f"every version of {term.dependency.complete_name}" if term.dependency.is_root: pretty_name: str = term.dependency.pretty_name return pretty_name if term.dependency.source_type: return str(term.dependency) pretty_name = term.dependency.complete_pretty_name return f"{pretty_name} ({term.dependency.pretty_constraint})" def _single_term_where(self, callable: Callable[[Term], bool]) -> Term | None: found = None for term in self._terms: if not callable(term): continue if found is not None: return None found = term return found def __repr__(self) -> str: return f"" ================================================ FILE: src/poetry/mixology/incompatibility_cause.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from poetry.mixology.incompatibility import Incompatibility class IncompatibilityCauseError(Exception): """ The reason and Incompatibility's terms are incompatible. """ class RootCauseError(IncompatibilityCauseError): pass class NoVersionsCauseError(IncompatibilityCauseError): pass class DependencyCauseError(IncompatibilityCauseError): pass class ConflictCauseError(IncompatibilityCauseError): """ The incompatibility was derived from two existing incompatibilities during conflict resolution. """ def __init__(self, conflict: Incompatibility, other: Incompatibility) -> None: self._conflict = conflict self._other = other @property def conflict(self) -> Incompatibility: return self._conflict @property def other(self) -> Incompatibility: return self._other def __str__(self) -> str: return str(self._conflict) class PythonCauseError(IncompatibilityCauseError): """ The incompatibility represents a package's python constraint (Python versions) being incompatible with the current python version. """ def __init__(self, python_version: str, root_python_version: str) -> None: self._python_version = python_version self._root_python_version = root_python_version @property def python_version(self) -> str: return self._python_version @property def root_python_version(self) -> str: return self._root_python_version class PlatformCauseError(IncompatibilityCauseError): """ The incompatibility represents a package's platform constraint (OS most likely) being incompatible with the current platform. """ def __init__(self, platform: str) -> None: self._platform = platform @property def platform(self) -> str: return self._platform ================================================ FILE: src/poetry/mixology/partial_solution.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.mixology.assignment import Assignment from poetry.mixology.set_relation import SetRelation if TYPE_CHECKING: from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.mixology.incompatibility import Incompatibility from poetry.mixology.term import Term class PartialSolution: """ # A list of Assignments that represent the solver's current best guess about # what's true for the eventual set of package versions that will comprise the # total solution. # # See: # https://github.com/dart-lang/mixology/tree/master/doc/solver.md#partial-solution. """ def __init__(self) -> None: # The assignments that have been made so far, in the order they were # assigned. self._assignments: list[Assignment] = [] # The decisions made for each package. self._decisions: dict[str, Package] = {} # The intersection of all positive Assignments for each package, minus any # negative Assignments that refer to that package. # # This is derived from self._assignments. self._positive: dict[str, Term] = {} # The union of all negative Assignments for each package. # # If a package has any positive Assignments, it doesn't appear in this # map. # # This is derived from self._assignments. self._negative: dict[str, Term] = {} # The number of distinct solutions that have been attempted so far. self._attempted_solutions = 1 # Whether the solver is currently backtracking. self._backtracking = False @property def decisions(self) -> list[Package]: return list(self._decisions.values()) @property def decision_level(self) -> int: return len(self._decisions) @property def attempted_solutions(self) -> int: return self._attempted_solutions @property def unsatisfied(self) -> list[Dependency]: return [ term.dependency for term in self._positive.values() if term.dependency.complete_name not in self._decisions ] def decide(self, package: Package) -> None: """ Adds an assignment of package as a decision and increments the decision level. """ # When we make a new decision after backtracking, count an additional # attempted solution. If we backtrack multiple times in a row, though, we # only want to count one, since we haven't actually started attempting a # new solution. if self._backtracking: self._attempted_solutions += 1 self._backtracking = False self._decisions[package.complete_name] = package self._assign( Assignment.decision(package, self.decision_level, len(self._assignments)) ) def derive( self, dependency: Dependency, is_positive: bool, cause: Incompatibility ) -> None: """ Adds an assignment of package as a derivation. """ self._assign( Assignment.derivation( dependency, is_positive, cause, self.decision_level, len(self._assignments), ) ) def _assign(self, assignment: Assignment) -> None: """ Adds an Assignment to _assignments and _positive or _negative. """ self._assignments.append(assignment) self._register(assignment) def backtrack(self, decision_level: int) -> None: """ Resets the current decision level to decision_level, and removes all assignments made after that level. """ self._backtracking = True packages = set() while self._assignments[-1].decision_level > decision_level: removed = self._assignments.pop(-1) packages.add(removed.dependency.complete_name) if removed.is_decision(): del self._decisions[removed.dependency.complete_name] # Re-compute _positive and _negative for the packages that were removed. for package in packages: if package in self._positive: del self._positive[package] if package in self._negative: del self._negative[package] for assignment in self._assignments: if assignment.dependency.complete_name in packages: self._register(assignment) def _register(self, assignment: Assignment) -> None: """ Registers an Assignment in _positive or _negative. """ name = assignment.dependency.complete_name old_positive = self._positive.get(name) if old_positive is not None: value = old_positive.intersect(assignment) assert value is not None self._positive[name] = value return old_negative = self._negative.get(name) term = ( assignment if old_negative is None else assignment.intersect(old_negative) ) assert term is not None if term.is_positive(): if name in self._negative: del self._negative[name] self._positive[name] = term else: self._negative[name] = term def satisfier(self, term: Term) -> Assignment: """ Returns the first Assignment in this solution such that the sublist of assignments up to and including that entry collectively satisfies term. """ assigned_term: Term | None = None for assignment in self._assignments: if assignment.dependency.complete_name != term.dependency.complete_name: continue if ( not assignment.dependency.is_root and not assignment.dependency.is_same_package_as(term.dependency) ): if not assignment.is_positive(): continue assert not term.is_positive() return assignment if assigned_term is None: assigned_term = assignment else: assigned_term = assigned_term.intersect(assignment) # As soon as we have enough assignments to satisfy term, return them. assert assigned_term is not None if assigned_term.satisfies(term): return assignment raise RuntimeError(f"[BUG] {term} is not satisfied.") def satisfies(self, term: Term) -> bool: return self.relation(term) == SetRelation.SUBSET def relation(self, term: Term) -> str: positive = self._positive.get(term.dependency.complete_name) if positive is not None: return positive.relation(term) negative = self._negative.get(term.dependency.complete_name) if negative is None: return SetRelation.OVERLAPPING return negative.relation(term) ================================================ FILE: src/poetry/mixology/result.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage class SolverResult: def __init__( self, root: ProjectPackage, packages: list[Package], attempted_solutions: int, ) -> None: self._root = root self._packages = packages self._attempted_solutions = attempted_solutions @property def packages(self) -> list[Package]: return self._packages @property def attempted_solutions(self) -> int: return self._attempted_solutions ================================================ FILE: src/poetry/mixology/set_relation.py ================================================ from __future__ import annotations class SetRelation: """ An enum of possible relationships between two sets. """ SUBSET = "subset" DISJOINT = "disjoint" OVERLAPPING = "overlapping" ================================================ FILE: src/poetry/mixology/term.py ================================================ from __future__ import annotations import functools from typing import TYPE_CHECKING from poetry.mixology.set_relation import SetRelation if TYPE_CHECKING: from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.dependency import Dependency class Term: """ A statement about a package which is true or false for a given selection of package versions. See https://github.com/dart-lang/pub/tree/master/doc/solver.md#term. """ def __init__(self, dependency: Dependency, is_positive: bool) -> None: self._dependency = dependency self._positive = is_positive self.relation = functools.lru_cache(maxsize=None)(self._relation) self.intersect = functools.lru_cache(maxsize=None)(self._intersect) @property def inverse(self) -> Term: return Term(self._dependency, not self.is_positive()) @property def dependency(self) -> Dependency: return self._dependency @property def constraint(self) -> VersionConstraint: return self._dependency.constraint def is_positive(self) -> bool: return self._positive def satisfies(self, other: Term) -> bool: """ Returns whether this term satisfies another. """ return ( self.dependency.complete_name == other.dependency.complete_name and self.relation(other) == SetRelation.SUBSET ) def _relation(self, other: Term) -> str: """ Returns the relationship between the package versions allowed by this term and another. """ if self.dependency.complete_name != other.dependency.complete_name: raise ValueError(f"{other} should refer to {self.dependency.complete_name}") other_constraint = other.constraint if other.is_positive(): if self.is_positive(): if not self._compatible_dependency(other.dependency): return SetRelation.DISJOINT # foo ^1.5.0 is a subset of foo ^1.0.0 if other_constraint.allows_all(self.constraint): return SetRelation.SUBSET # foo ^2.0.0 is disjoint with foo ^1.0.0 if not self.constraint.allows_any(other_constraint): return SetRelation.DISJOINT return SetRelation.OVERLAPPING else: if not self._compatible_dependency(other.dependency): return SetRelation.OVERLAPPING # not foo ^1.0.0 is disjoint with foo ^1.5.0 if self.constraint.allows_all(other_constraint): return SetRelation.DISJOINT # not foo ^1.5.0 overlaps foo ^1.0.0 # not foo ^2.0.0 is a superset of foo ^1.5.0 return SetRelation.OVERLAPPING else: if self.is_positive(): if not self._compatible_dependency(other.dependency): return SetRelation.SUBSET # foo ^2.0.0 is a subset of not foo ^1.0.0 if not other_constraint.allows_any(self.constraint): return SetRelation.SUBSET # foo ^1.5.0 is disjoint with not foo ^1.0.0 if ( other_constraint.allows_all(self.constraint) # if transitive markers are not equal we have to handle it # as overlapping so that markers are merged later and self.dependency.transitive_marker == other.dependency.transitive_marker ): return SetRelation.DISJOINT # foo ^1.0.0 overlaps not foo ^1.5.0 return SetRelation.OVERLAPPING else: if not self._compatible_dependency(other.dependency): return SetRelation.OVERLAPPING # not foo ^1.0.0 is a subset of not foo ^1.5.0 if self.constraint.allows_all(other_constraint): return SetRelation.SUBSET # not foo ^2.0.0 overlaps not foo ^1.0.0 # not foo ^1.5.0 is a superset of not foo ^1.0.0 return SetRelation.OVERLAPPING def _intersect(self, other: Term) -> Term | None: """ Returns a Term that represents the packages allowed by both this term and another """ if self.dependency.complete_name != other.dependency.complete_name: raise ValueError(f"{other} should refer to {self.dependency.complete_name}") if self._compatible_dependency(other.dependency): if self.is_positive() != other.is_positive(): # foo ^1.0.0 ∩ not foo ^1.5.0 → foo >=1.0.0 <1.5.0 positive = self if self.is_positive() else other negative = other if self.is_positive() else self return self._non_empty_term( positive.constraint.difference(negative.constraint), True, other ) elif self.is_positive(): # foo ^1.0.0 ∩ foo >=1.5.0 <3.0.0 → foo ^1.5.0 return self._non_empty_term( self.constraint.intersect(other.constraint), True, other ) else: # not foo ^1.0.0 ∩ not foo >=1.5.0 <3.0.0 → not foo >=1.0.0 <3.0.0 return self._non_empty_term( self.constraint.union(other.constraint), False, other ) elif self.is_positive() != other.is_positive(): return self if self.is_positive() else other else: return None def difference(self, other: Term) -> Term | None: """ Returns a Term that represents packages allowed by this term and not by the other """ return self.intersect(other.inverse) def _compatible_dependency(self, other: Dependency) -> bool: return ( self.dependency.is_root or other.is_root or other.is_same_package_as(self.dependency) or ( # we do this here to indicate direct origin dependencies are # compatible with NVR dependencies self.dependency.complete_name == other.complete_name and self.dependency.is_direct_origin() != other.is_direct_origin() ) ) def _non_empty_term( self, constraint: VersionConstraint, is_positive: bool, other: Term ) -> Term | None: if constraint.is_empty(): return None # when creating a new term prefer direct-reference dependencies dependency = ( other.dependency if not self.dependency.is_direct_origin() and other.dependency.is_direct_origin() else self.dependency ) new_dep = dependency.with_constraint(constraint) if is_positive and other.is_positive(): new_dep.transitive_marker = self.dependency.transitive_marker.union( other.dependency.transitive_marker ) return Term(new_dep, is_positive) def __str__(self) -> str: prefix = "not " if not self.is_positive() else "" return f"{prefix}{self._dependency}" def __repr__(self) -> str: return f"" ================================================ FILE: src/poetry/mixology/version_solver.py ================================================ from __future__ import annotations import collections import functools import time from enum import IntEnum from typing import TYPE_CHECKING from poetry.core.packages.dependency import Dependency from poetry.mixology.failure import SolveFailureError from poetry.mixology.incompatibility import Incompatibility from poetry.mixology.incompatibility_cause import ConflictCauseError from poetry.mixology.incompatibility_cause import NoVersionsCauseError from poetry.mixology.incompatibility_cause import RootCauseError from poetry.mixology.partial_solution import PartialSolution from poetry.mixology.result import SolverResult from poetry.mixology.set_relation import SetRelation from poetry.mixology.term import Term from poetry.packages import PackageCollection if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage from poetry.packages import DependencyPackage from poetry.puzzle.provider import Provider _conflict = object() class Preference(IntEnum): """ Preference is one of the criteria for choosing which dependency to solve first. A higher value means that there are "more options" to satisfy a dependency. A lower value takes precedence. """ DIRECT_ORIGIN = 0 NO_CHOICE = 1 USE_LATEST = 2 LOCKED = 3 DEFAULT = 4 CompKey = tuple[Preference, int, bool, int] DependencyCacheKey = tuple[str, str | None, str | None, str | None, str | None] class DependencyCache: """ A cache of the valid dependencies. The key observation here is that during the search - except at backtracking - once we have decided that a dependency is invalid, we never need check it again. """ def __init__(self, provider: Provider) -> None: self._provider = provider # self._cache maps a package name to a stack of cached package lists, # ordered by the decision level which added them to the cache. This is # done so that when backtracking we can maintain cache entries from # previous decision levels, while clearing cache entries from only the # rolled back levels. # # In order to maintain the integrity of the cache, `clear_level()` # needs to be called in descending order as decision levels are # backtracked so that the correct items can be popped from the stack. self._cache: dict[DependencyCacheKey, list[list[DependencyPackage]]] = ( collections.defaultdict(list) ) self._cached_dependencies_by_level: dict[int, list[DependencyCacheKey]] = ( collections.defaultdict(list) ) self._search_for_cached = functools.lru_cache(maxsize=128)(self._search_for) def _search_for( self, dependency: Dependency, key: DependencyCacheKey, ) -> list[DependencyPackage]: cache_entries = self._cache[key] if cache_entries: packages = [ p for p in cache_entries[-1] if dependency.constraint.allows(p.package.version) ] else: packages = None # provider.search_for() normally does not include pre-release packages # (unless requested), but will include them if there are no other # eligible package versions for a version constraint. # # Therefore, if the eligible versions have been filtered down to # nothing, we need to call provider.search_for() again as it may return # additional results this time. if not packages: packages = self._provider.search_for(dependency) return packages def search_for( self, dependency: Dependency, decision_level: int, ) -> list[DependencyPackage]: key = ( dependency.name, dependency.source_type, dependency.source_url, dependency.source_reference, dependency.source_subdirectory, ) # We could always use dependency.without_features() here, # but for performance reasons we only do it if necessary. packages = self._search_for_cached( dependency.without_features() if dependency.features else dependency, key ) if not self._cache[key] or self._cache[key][-1] is not packages: self._cache[key].append(packages) self._cached_dependencies_by_level[decision_level].append(key) if dependency.features and packages: # Use the cached dependency so that a possible explicit source is set. return PackageCollection( packages[0].dependency.with_features(dependency.features), packages ) return packages def clear_level(self, level: int) -> None: if level in self._cached_dependencies_by_level: self._search_for_cached.cache_clear() for key in self._cached_dependencies_by_level.pop(level): self._cache[key].pop() class VersionSolver: """ The version solver that finds a set of package versions that satisfy the root package's dependencies. See https://github.com/dart-lang/pub/tree/master/doc/solver.md for details on how this solver works. """ def __init__(self, root: ProjectPackage, provider: Provider) -> None: self._root = root self._provider = provider self._dependency_cache = DependencyCache(provider) self._incompatibilities: dict[str, list[Incompatibility]] = {} self._contradicted_incompatibilities: set[Incompatibility] = set() self._contradicted_incompatibilities_by_level: dict[ int, set[Incompatibility] ] = collections.defaultdict(set) self._solution = PartialSolution() self._get_comp_key_cached = functools.cache(self._get_comp_key) @property def solution(self) -> PartialSolution: return self._solution def solve(self) -> SolverResult: """ Finds a set of dependencies that match the root package's constraints, or raises an error if no such set is available. """ start = time.time() root_dependency = Dependency(self._root.name, self._root.version) root_dependency.is_root = True self._add_incompatibility( Incompatibility([Term(root_dependency, False)], RootCauseError()) ) try: next: str | None = self._root.name while next is not None: self._propagate(next) next = self._choose_package_version() return self._result() except Exception: raise finally: self._log( f"Version solving took {time.time() - start:.3f} seconds.\n" f"Tried {self._solution.attempted_solutions} solutions." ) def _propagate(self, package: str) -> None: """ Performs unit propagation on incompatibilities transitively related to package to derive new assignments for _solution. """ changed = {package} while changed: package = changed.pop() # Iterate in reverse because conflict resolution tends to produce more # general incompatibilities as time goes on. If we look at those first, # we can derive stronger assignments sooner and more eagerly find # conflicts. for incompatibility in reversed(self._incompatibilities[package]): if incompatibility in self._contradicted_incompatibilities: continue result = self._propagate_incompatibility(incompatibility) if result is _conflict: # If the incompatibility is satisfied by the solution, we use # _resolve_conflict() to determine the root cause of the conflict as # a new incompatibility. # # It also backjumps to a point in the solution # where that incompatibility will allow us to derive new assignments # that avoid the conflict. root_cause = self._resolve_conflict(incompatibility) # Back jumping erases all the assignments we did at the previous # decision level, so we clear [changed] and refill it with the # newly-propagated assignment. changed.clear() result = self._propagate_incompatibility(root_cause) assert result is not None assert result != _conflict assert isinstance(result, str) changed.add(result) break if result is not None: assert isinstance(result, str) changed.add(result) def _propagate_incompatibility( self, incompatibility: Incompatibility ) -> str | object | None: """ If incompatibility is almost satisfied by _solution, adds the negation of the unsatisfied term to _solution. If incompatibility is satisfied by _solution, returns _conflict. If incompatibility is almost satisfied by _solution, returns the unsatisfied term's package name. Otherwise, returns None. """ # The first entry in incompatibility.terms that's not yet satisfied by # _solution, if one exists. If we find more than one, _solution is # inconclusive for incompatibility and we can't deduce anything. unsatisfied = None for term in incompatibility.terms: relation = self._solution.relation(term) if relation == SetRelation.DISJOINT: # If term is already contradicted by _solution, then # incompatibility is contradicted as well and there's nothing new we # can deduce from it. self._contradicted_incompatibilities.add(incompatibility) self._contradicted_incompatibilities_by_level[ self._solution.decision_level ].add(incompatibility) return None elif relation == SetRelation.OVERLAPPING: # If more than one term is inconclusive, we can't deduce anything about # incompatibility. if unsatisfied is not None: return None # If exactly one term in incompatibility is inconclusive, then it's # almost satisfied and [term] is the unsatisfied term. We can add the # inverse of the term to _solution. unsatisfied = term # If *all* terms in incompatibility are satisfied by _solution, then # incompatibility is satisfied and we have a conflict. if unsatisfied is None: return _conflict self._contradicted_incompatibilities.add(incompatibility) self._contradicted_incompatibilities_by_level[ self._solution.decision_level ].add(incompatibility) adverb = "not " if unsatisfied.is_positive() else "" self._log(f"derived: {adverb}{unsatisfied.dependency}") self._solution.derive( unsatisfied.dependency, not unsatisfied.is_positive(), incompatibility ) complete_name: str = unsatisfied.dependency.complete_name return complete_name def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility: """ Given an incompatibility that's satisfied by _solution, The `conflict resolution`_ constructs a new incompatibility that encapsulates the root cause of the conflict and backtracks _solution until the new incompatibility will allow _propagate() to deduce new assignments. Adds the new incompatibility to _incompatibilities and returns it. .. _conflict resolution: https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution """ self._log(f"conflict: {incompatibility}") new_incompatibility = False while not incompatibility.is_failure(): # The term in incompatibility.terms that was most recently satisfied by # _solution. most_recent_term = None # The earliest assignment in _solution such that incompatibility is # satisfied by _solution up to and including this assignment. most_recent_satisfier = None # The difference between most_recent_satisfier and most_recent_term; # that is, the versions that are allowed by most_recent_satisfier and not # by most_recent_term. This is None if most_recent_satisfier totally # satisfies most_recent_term. difference = None # The decision level of the earliest assignment in _solution *before* # most_recent_satisfier such that incompatibility is satisfied by # _solution up to and including this assignment plus # most_recent_satisfier. # # Decision level 1 is the level where the root package was selected. It's # safe to go back to decision level 0, but stopping at 1 tends to produce # better error messages, because references to the root package end up # closer to the final conclusion that no solution exists. previous_satisfier_level = 1 for term in incompatibility.terms: satisfier = self._solution.satisfier(term) if most_recent_satisfier is None: most_recent_term = term most_recent_satisfier = satisfier elif most_recent_satisfier.index < satisfier.index: previous_satisfier_level = max( previous_satisfier_level, most_recent_satisfier.decision_level ) most_recent_term = term most_recent_satisfier = satisfier difference = None else: previous_satisfier_level = max( previous_satisfier_level, satisfier.decision_level ) if most_recent_term == term: # If most_recent_satisfier doesn't satisfy most_recent_term on its # own, then the next-most-recent satisfier may be the one that # satisfies the remainder. difference = most_recent_satisfier.difference(most_recent_term) if difference is not None: previous_satisfier_level = max( previous_satisfier_level, self._solution.satisfier(difference.inverse).decision_level, ) # If most_recent_identifier is the only satisfier left at its decision # level, or if it has no cause (indicating that it's a decision rather # than a derivation), then incompatibility is the root cause. We then # backjump to previous_satisfier_level, where incompatibility is # guaranteed to allow _propagate to produce more assignments. # using assert to suppress mypy [union-attr] assert most_recent_satisfier is not None if ( previous_satisfier_level < most_recent_satisfier.decision_level or most_recent_satisfier.cause is None ): for level in range( self._solution.decision_level, previous_satisfier_level, -1 ): if level in self._contradicted_incompatibilities_by_level: self._contradicted_incompatibilities.difference_update( self._contradicted_incompatibilities_by_level.pop(level), ) self._dependency_cache.clear_level(level) self._solution.backtrack(previous_satisfier_level) if new_incompatibility: self._add_incompatibility(incompatibility) return incompatibility # Create a new incompatibility by combining incompatibility with the # incompatibility that caused most_recent_satisfier to be assigned. Doing # this iteratively constructs an incompatibility that's guaranteed to be # true (that is, we know for sure no solution will satisfy the # incompatibility) while also approximating the intuitive notion of the # "root cause" of the conflict. new_terms = [ term for term in incompatibility.terms if term != most_recent_term ] for term in most_recent_satisfier.cause.terms: if term.dependency != most_recent_satisfier.dependency: new_terms.append(term) # The most_recent_satisfier may not satisfy most_recent_term on its own # if there are a collection of constraints on most_recent_term that # only satisfy it together. For example, if most_recent_term is # `foo ^1.0.0` and _solution contains `[foo >=1.0.0, # foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even # though it doesn't totally satisfy `foo ^1.0.0`. # # In this case, we add `not (most_recent_satisfier \ most_recent_term)` to # the incompatibility as well, See the `algorithm documentation`_ for # details. # # .. _algorithm documentation: # https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution if difference is not None: inverse = difference.inverse if inverse.dependency != most_recent_satisfier.dependency: new_terms.append(inverse) incompatibility = Incompatibility( new_terms, ConflictCauseError(incompatibility, most_recent_satisfier.cause), ) new_incompatibility = True partially = "" if difference is None else " partially" self._log( f"! {most_recent_term} is{partially} satisfied by" f" {most_recent_satisfier}" ) self._log(f'! which is caused by "{most_recent_satisfier.cause}"') self._log(f"! thus: {incompatibility}") raise SolveFailureError(incompatibility) def _get_comp_key(self, dependency: Dependency) -> CompKey: """ Returns a tuple of - preference - num_deps_upper_bound - has_deps - num_packages that serves as priority for choosing the next package to resolve. (A lower value takes precedence.) In order to provide results that are as deterministic as possible and consistent between `poetry lock` and `poetry update`, the return value of two different dependencies should not be equal if possible. ## preference See Preference class. ## num_deps_upper_bound A dependency with an upper bound is more likely to cause conflicts. Therefore, a package with more dependencies with upper bounds should be chosen first. ## has_deps A package with dependencies should be chosen first because a package without dependencies is less likely to cause conflicts. ## num_packages The original algorithm proposes to prefer packages with as few remaining versions as possible, so that if a conflict is necessary it's forced quickly. https://github.com/dart-lang/pub/blob/master/doc/solver.md#decision-making However, this leads to the famous boto3 vs. urllib3 issue, so we prefer packages with more remaining versions (see https://github.com/python-poetry/poetry/pull/8255#issuecomment-1657198242 for more details). """ preference = Preference.DEFAULT # Direct origin dependencies must be handled first: we don't want to resolve # a regular dependency for some package only to find later that we had a # direct-origin dependency. if dependency.is_direct_origin(): preference = Preference.DIRECT_ORIGIN packages: list[DependencyPackage] = [] use_latest = dependency.name in self._provider.use_latest if not use_latest: locked = self._provider.get_locked(dependency) if locked: if preference == Preference.DEFAULT: preference = Preference.LOCKED packages = [locked] if not packages: packages = self._dependency_cache.search_for( dependency, self._solution.decision_level ) num_packages = len(packages) if packages: package = packages[0].package if package.is_root(): relevant_dependencies = package.all_requires else: if preference != Preference.LOCKED and not package.is_direct_origin(): # We have to get the package from the pool, # otherwise `requires` will be empty. # # We might need `package.source_reference` as fallback # for transitive dependencies without a source # if there is a top-level dependency # for the same package with an explicit source. for repo in (dependency.source_name, package.source_reference): try: package = self._provider.get_package_from_pool( package.pretty_name, package.version, repository_name=repo, ) except Exception: pass else: break relevant_dependencies = [ r for r in package.requires if not r.in_extras or r.in_extras[0] in dependency.extras ] has_deps = bool(relevant_dependencies) num_deps_upper_bound = sum( 1 for d in relevant_dependencies if d.constraint.has_upper_bound() ) else: has_deps = False num_deps_upper_bound = 0 if preference == Preference.DEFAULT: if num_packages < 2: preference = Preference.NO_CHOICE elif use_latest: preference = Preference.USE_LATEST return preference, -num_deps_upper_bound, not has_deps, -num_packages def _choose_next(self, unsatisfied: list[Dependency]) -> Dependency: """ Chooses the next package to resolve. """ return min(unsatisfied, key=self._get_comp_key_cached) def _choose_package_version(self) -> str | None: """ Tries to select a version of a required package. Returns the name of the package whose incompatibilities should be propagated by _propagate(), or None indicating that version solving is complete and a solution has been found. """ unsatisfied = self._solution.unsatisfied if not unsatisfied: return None dependency = self._choose_next(unsatisfied) locked = self._provider.get_locked(dependency) if locked is None: packages = self._dependency_cache.search_for( dependency, self._solution.decision_level ) package = next(iter(packages), None) if package is None: # If there are no versions that satisfy the constraint, # add an incompatibility that indicates that. self._add_incompatibility( Incompatibility([Term(dependency, True)], NoVersionsCauseError()) ) complete_name = dependency.complete_name return complete_name package.dependency.transitive_marker = dependency.transitive_marker else: package = locked package = self._provider.complete_package(package) conflict = False for incompatibility in self._provider.incompatibilities_for(package): self._add_incompatibility(incompatibility) # If an incompatibility is already satisfied, then selecting version # would cause a conflict. # # We'll continue adding its dependencies, then go back to # unit propagation which will guide us to choose a better version. conflict = conflict or all( term.dependency.complete_name == dependency.complete_name or self._solution.satisfies(term) for term in incompatibility.terms ) if not conflict: self._solution.decide(package.package) self._log( f"selecting {package.package.complete_name}" f" ({package.package.full_pretty_version})" ) complete_name = dependency.complete_name return complete_name def _result(self) -> SolverResult: """ Creates a #SolverResult from the decisions in _solution """ decisions = self._solution.decisions return SolverResult( self._root, [p for p in decisions if not p.is_root()], self._solution.attempted_solutions, ) def _add_incompatibility(self, incompatibility: Incompatibility) -> None: self._log(f"fact: {incompatibility}") for term in incompatibility.terms: if term.dependency.complete_name not in self._incompatibilities: self._incompatibilities[term.dependency.complete_name] = [] if ( incompatibility in self._incompatibilities[term.dependency.complete_name] ): continue self._incompatibilities[term.dependency.complete_name].append( incompatibility ) def _log(self, text: str) -> None: self._provider.debug(text, self._solution.attempted_solutions) ================================================ FILE: src/poetry/packages/__init__.py ================================================ from __future__ import annotations from poetry.packages.dependency_package import DependencyPackage from poetry.packages.locker import Locker from poetry.packages.package_collection import PackageCollection __all__ = ["DependencyPackage", "Locker", "PackageCollection"] ================================================ FILE: src/poetry/packages/dependency_package.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Iterable from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package class DependencyPackage: def __init__(self, dependency: Dependency, package: Package) -> None: self._dependency = dependency self._package = package @property def dependency(self) -> Dependency: return self._dependency @property def package(self) -> Package: return self._package def clone(self) -> DependencyPackage: return self.__class__(self._dependency, self._package.clone()) def with_features(self, features: Iterable[str]) -> DependencyPackage: return self.__class__(self._dependency, self._package.with_features(features)) def without_features(self) -> DependencyPackage: return self.with_features([]) def __str__(self) -> str: return str(self._package) def __repr__(self) -> str: return repr(self._package) def __hash__(self) -> int: return hash(self._package) def __eq__(self, other: object) -> bool: if isinstance(other, DependencyPackage): other = other.package equal: bool = self._package == other return equal ================================================ FILE: src/poetry/packages/direct_origin.py ================================================ from __future__ import annotations import functools from pathlib import Path from typing import TYPE_CHECKING from poetry.core.packages.utils.link import Link from poetry.config.config import Config from poetry.inspection.info import PackageInfo from poetry.inspection.info import PackageInfoError from poetry.utils.authenticator import get_default_authenticator from poetry.utils.helpers import download_file from poetry.utils.helpers import get_file_hash from poetry.vcs.git import Git if TYPE_CHECKING: from poetry.core.packages.package import Package from poetry.utils.cache import ArtifactCache @functools.cache def _get_package_from_git( url: str, branch: str | None = None, tag: str | None = None, rev: str | None = None, subdirectory: str | None = None, source_root: Path | None = None, ) -> Package: source = Git.clone( url=url, source_root=source_root, branch=branch, tag=tag, revision=rev, clean=False, ) revision = Git.get_revision(source) path = Path(source.path) if subdirectory: path = path.joinpath(subdirectory) package = DirectOrigin.get_package_from_directory(path) package._source_type = "git" package._source_url = url package._source_reference = rev or tag or branch or "HEAD" package._source_resolved_reference = revision package._source_subdirectory = subdirectory return package class DirectOrigin: def __init__(self, artifact_cache: ArtifactCache) -> None: self._artifact_cache = artifact_cache config = Config.create() self._max_retries = config.get("requests.max-retries", 0) self._authenticator = get_default_authenticator() @classmethod def get_package_from_file(cls, file_path: Path) -> Package: try: package = PackageInfo.from_path(path=file_path).to_package( root_dir=file_path ) except PackageInfoError: raise RuntimeError( f"Unable to determine package info from path: {file_path}" ) package.files = [ { "file": file_path.name, "hash": "sha256:" + get_file_hash(file_path), "size": file_path.stat().st_size, } ] return package @classmethod def get_package_from_directory(cls, directory: Path) -> Package: return PackageInfo.from_directory(path=directory).to_package(root_dir=directory) def _download_file(self, url: str, dest: Path) -> None: download_file( url, dest, session=self._authenticator, max_retries=self._max_retries ) def get_package_from_url(self, url: str) -> Package: link = Link(url) artifact = self._artifact_cache.get_cached_archive_for_link( link, strict=True, download_func=self._download_file ) package = self.get_package_from_file(artifact) package._source_type = "url" package._source_url = url return package @staticmethod def get_package_from_vcs( vcs: str, url: str, branch: str | None = None, tag: str | None = None, rev: str | None = None, subdirectory: str | None = None, source_root: Path | None = None, ) -> Package: if vcs != "git": raise ValueError(f"Unsupported VCS dependency {vcs}") return _get_package_from_git( url=url, branch=branch, tag=tag, rev=rev, subdirectory=subdirectory, source_root=source_root, ) ================================================ FILE: src/poetry/packages/locker.py ================================================ from __future__ import annotations import json import logging import os import re import warnings from hashlib import sha256 from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from typing import cast from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.version.exceptions import InvalidVersionError from poetry.core.version.markers import parse_marker from poetry.core.version.requirements import InvalidRequirementError from tomlkit import array from tomlkit import comment from tomlkit import document from tomlkit import inline_table from tomlkit import table from poetry.__version__ import __version__ from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.packages.transitive_package_info import group_sort_key from poetry.toml.file import TOMLFile from poetry.utils._compat import tomllib if TYPE_CHECKING: from packaging.utils import NormalizedName from poetry.core.packages.directory_dependency import DirectoryDependency from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.vcs_dependency import VCSDependency from tomlkit.toml_document import TOMLDocument from poetry.repositories.lockfile_repository import LockfileRepository logger = logging.getLogger(__name__) _GENERATED_IDENTIFIER = "@" + "generated" GENERATED_COMMENT = ( f"This file is automatically {_GENERATED_IDENTIFIER} by Poetry" f" {__version__} and should not be changed by hand." ) class Locker: _VERSION = "2.1" _READ_VERSION_RANGE = ">=1,<3" _legacy_keys: ClassVar[list[str]] = [ "dependencies", "source", "extras", "dev-dependencies", ] _relevant_keys: ClassVar[list[str]] = [*_legacy_keys, "group"] _relevant_project_keys: ClassVar[list[str]] = [ "requires-python", "dependencies", "optional-dependencies", ] def __init__(self, lock: Path, pyproject_data: dict[str, Any]) -> None: self._lock = lock self._pyproject_data = pyproject_data self._lock_data: dict[str, Any] | None = None self._content_hash = self._get_content_hash() @property def lock(self) -> Path: return self._lock @property def lock_data(self) -> dict[str, Any]: if self._lock_data is None: self._lock_data = self._get_lock_data() return self._lock_data def is_locked(self) -> bool: """ Checks whether the locker has been locked (lockfile found). """ return self._lock.exists() def is_fresh(self) -> bool: """ Checks whether the lock file is still up to date with the current hash. """ with self.lock.open("rb") as f: lock = tomllib.load(f) metadata = lock.get("metadata", {}) if "content-hash" in metadata: fresh: bool = self._content_hash == metadata["content-hash"] if not fresh: with self.lock.open("r", encoding="utf-8") as f: generated_comment = f.readline() if m := re.search("Poetry ([^ ]+)", generated_comment): try: version = Version.parse(m.group(1)) except InvalidVersionError: pass else: if version < Version.parse("2.3.0"): # Before Poetry 2.3.0, the content hash did not include # dependency groups, so we need to recompute it without # them for comparison. old_content_hash = self._get_content_hash( with_dependency_groups=False ) fresh = old_content_hash == metadata["content-hash"] return fresh return False def is_locked_groups_and_markers(self) -> bool: if not self.is_locked(): return False version = Version.parse(self.lock_data["metadata"]["lock-version"]) return version >= Version.parse("2.1") def set_pyproject_data(self, pyproject_data: dict[str, Any]) -> None: self._pyproject_data = pyproject_data self._content_hash = self._get_content_hash() def set_local_config(self, local_config: dict[str, Any]) -> None: warnings.warn( "Locker.set_local_config() is deprecated and will be removed in a future" " release. Use Locker.set_pyproject_data() instead.", DeprecationWarning, stacklevel=2, ) self._pyproject_data.setdefault("tool", {})["poetry"] = local_config self._content_hash = self._get_content_hash() def locked_repository(self) -> LockfileRepository: """ Searches and returns a repository of locked packages. """ from poetry.repositories.lockfile_repository import LockfileRepository repository = LockfileRepository() if not self.is_locked(): return repository locked_packages = cast("list[dict[str, Any]]", self.lock_data["package"]) if not locked_packages: return repository for info in locked_packages: repository.add_package(self._get_locked_package(info)) return repository def locked_packages(self) -> dict[Package, TransitivePackageInfo]: if not self.is_locked_groups_and_markers(): raise RuntimeError( "This method should not be called if the lock file" " is not at least version 2.1." ) locked_packages: dict[Package, TransitivePackageInfo] = {} locked_package_info = cast("list[dict[str, Any]]", self.lock_data["package"]) for info in locked_package_info: package = self._get_locked_package(info, with_dependencies=False) groups = set(info["groups"]) locked_marker = info.get("markers", "*") if isinstance(locked_marker, str): markers = { canonicalize_name(group): parse_marker(locked_marker) for group in groups } else: markers = { canonicalize_name(group): parse_marker( locked_marker.get(group, "*") ) for group in groups } locked_packages[package] = TransitivePackageInfo( 0, {canonicalize_name(g) for g in groups}, markers ) return locked_packages def set_lock_data( self, root: Package, packages: dict[Package, TransitivePackageInfo] ) -> bool: """Store lock data and eventually persist to the lock file""" lock = self._compute_lock_data(root, packages) if self._should_write(lock): self._write_lock_data(lock) return True return False def _compute_lock_data( self, root: Package, packages: dict[Package, TransitivePackageInfo] ) -> TOMLDocument: package_specs = self._lock_packages(packages) # Retrieving hashes for package in package_specs: files = array() for f in package["files"]: file_metadata = inline_table() for k, v in sorted(f.items()): file_metadata[k] = v files.append(file_metadata) package["files"] = files.multiline(True) lock = document() lock.add(comment(GENERATED_COMMENT)) lock["package"] = package_specs if root.extras: lock["extras"] = { extra: sorted(dep.pretty_name for dep in deps) for extra, deps in sorted(root.extras.items()) } lock["metadata"] = { "lock-version": self._VERSION, "python-versions": root.python_versions, "content-hash": self._content_hash, } return lock def _should_write(self, lock: TOMLDocument) -> bool: # if lock file exists: compare with existing lock data do_write = True if self.is_locked(): try: lock_data = self.lock_data except RuntimeError: # incompatible, invalid or no lock file pass else: do_write = lock != lock_data return do_write def _write_lock_data(self, data: TOMLDocument) -> None: if self.lock.exists(): # The following code is roughly equivalent to # • lockfile = TOMLFile(self.lock) # • lockfile.read() # • lockfile.write(data) # However, lockfile.read() takes more than half a second even # for a modestly sized project like Poetry itself and the only reason # for reading the lockfile is to determine the line endings. Thus, # we do that part for ourselves here, which only takes about 10 ms. # get original line endings with open(self.lock, encoding="utf-8", newline="") as f: line = f.readline() linesep = "\r\n" if line.endswith("\r\n") else "\n" # enforce original line endings content = data.as_string() if linesep == "\n": content = content.replace("\r\n", "\n") elif linesep == "\r\n": content = re.sub(r"(? str: """ Returns the sha256 hash of the sorted content of the pyproject file. """ project_content = self._pyproject_data.get("project", {}) group_content = ( self._pyproject_data.get("dependency-groups", {}) if with_dependency_groups else {} ) tool_poetry_content = self._pyproject_data.get("tool", {}).get("poetry", {}) relevant_project_content = {} for key in self._relevant_project_keys: data = project_content.get(key) if data is not None: relevant_project_content[key] = data relevant_poetry_content = {} for key in self._relevant_keys: data = tool_poetry_content.get(key) if data is None and ( # Special handling for legacy keys is just for backwards compatibility, # and thereby not required if there is relevant content in [project] # or [dependency-groups]. key not in self._legacy_keys or relevant_project_content or group_content ): continue relevant_poetry_content[key] = data relevant_content = {} if relevant_project_content: relevant_content["project"] = relevant_project_content if group_content: # For backwards compatibility, we must not add dependency-groups # if it is empty. relevant_content["dependency-groups"] = group_content if relevant_content: relevant_content["tool"] = {"poetry": relevant_poetry_content} else: # For backwards compatibility, we have to put the relevant content # of the [tool.poetry] section at top level! relevant_content = relevant_poetry_content return sha256(json.dumps(relevant_content, sort_keys=True).encode()).hexdigest() def _get_lock_data(self) -> dict[str, Any]: if not self.lock.exists(): raise RuntimeError("No lockfile found. Unable to read locked packages") with self.lock.open("rb") as f: try: lock_data = tomllib.load(f) except tomllib.TOMLDecodeError as e: raise RuntimeError(f"Unable to read the lock file ({e}).") # if the lockfile doesn't contain a metadata section at all, # it probably needs to be rebuilt completely if "metadata" not in lock_data: raise RuntimeError( "The lock file does not have a metadata entry.\n" "Regenerate the lock file with the `poetry lock` command." ) metadata = lock_data["metadata"] if "lock-version" not in metadata: raise RuntimeError( "The lock file is not compatible with the current version of Poetry.\n" "Regenerate the lock file with the `poetry lock` command." ) lock_version = Version.parse(metadata["lock-version"]) current_version = Version.parse(self._VERSION) accepted_versions = parse_constraint(self._READ_VERSION_RANGE) lock_version_allowed = accepted_versions.allows(lock_version) if lock_version_allowed and current_version < lock_version: logger.warning( "The lock file might not be compatible with the current version of" " Poetry.\nUpgrade Poetry to ensure the lock file is read properly or," " alternatively, regenerate the lock file with the `poetry lock`" " command." ) elif not lock_version_allowed: raise RuntimeError( "The lock file is not compatible with the current version of Poetry.\n" "Upgrade Poetry to be able to read the lock file or, alternatively, " "regenerate the lock file with the `poetry lock` command." ) return lock_data def _get_locked_package( self, info: dict[str, Any], with_dependencies: bool = True ) -> Package: source = info.get("source", {}) source_type = source.get("type") url = source.get("url") if source_type in ["directory", "file"]: url = self.lock.parent.joinpath(url).resolve().as_posix() name = info["name"] package = Package( name, info["version"], source_type=source_type, source_url=url, source_reference=source.get("reference"), source_resolved_reference=source.get("resolved_reference"), source_subdirectory=source.get("subdirectory"), ) package.description = info.get("description", "") package.optional = info["optional"] metadata = cast("dict[str, Any]", self.lock_data["metadata"]) # Storing of package files and hashes has been through a few generations in # the lockfile, we can read them all: # # - latest and preferred is that this is read per package, from # package.files # - oldest is that hashes were stored in metadata.hashes without filenames # - in between those two, hashes were stored alongside filenames in # metadata.files package_files = info.get("files") if package_files is not None: package.files = package_files elif "hashes" in metadata: hashes = cast("dict[str, Any]", metadata["hashes"]) # Strictly speaking, this is not correct, # but we do not know the file names here, # so we just set both file and hash. package.files = [{"file": h, "hash": h} for h in hashes[name]] elif source_type in {"git", "directory", "url"}: package.files = [] else: files = metadata["files"][name] if source_type == "file": filename = Path(url).name package.files = [item for item in files if item["file"] == filename] else: # Strictly speaking, this is not correct, but we have no chance # to always determine which are the correct files because the # lockfile doesn't keep track which files belong to which package. package.files = files package.python_versions = info["python-versions"] if "develop" in info: package.develop = info["develop"] if with_dependencies: from poetry.factory import Factory package_extras: dict[NormalizedName, list[Dependency]] = {} extras = info.get("extras", {}) if extras: for name, deps in extras.items(): name = canonicalize_name(name) package_extras[name] = [] for dep in deps: try: dependency = Dependency.create_from_pep_508(dep) except InvalidRequirementError: # handle lock files with invalid PEP 508 m = re.match(r"^(.+?)(?:\[(.+?)])?(?:\s+\((.+)\))?$", dep) if not m: raise dep_name = m.group(1) extras = m.group(2) or "" constraint = m.group(3) or "*" dependency = Dependency( dep_name, constraint, extras=extras.split(",") ) package_extras[name].append(dependency) package.extras = package_extras for dep_name, constraint in info.get("dependencies", {}).items(): root_dir = self.lock.parent if package.source_type == "directory": # root dir should be the source of the package relative to the lock # path assert package.source_url is not None root_dir = Path(package.source_url) if isinstance(constraint, list): for c in constraint: package.add_dependency( Factory.create_dependency(dep_name, c, root_dir=root_dir) ) continue package.add_dependency( Factory.create_dependency(dep_name, constraint, root_dir=root_dir) ) return package def _lock_packages( self, packages: dict[Package, TransitivePackageInfo] ) -> list[dict[str, Any]]: locked = [] for package in sorted( packages, key=lambda x: ( x.name, x.version, x.source_type or "", x.source_url or "", x.source_subdirectory or "", x.source_reference or "", x.source_resolved_reference or "", ), ): spec = self._dump_package(package, packages[package]) locked.append(spec) return locked def _dump_package( self, package: Package, transitive_info: TransitivePackageInfo ) -> dict[str, Any]: dependencies: dict[str, list[Any]] = {} for dependency in sorted( package.requires, key=lambda d: ( d.name, str(d.marker) if not d.marker.is_any() else "", ), ): dependencies.setdefault(dependency.pretty_name, []) constraint = inline_table() if dependency.is_directory(): dependency = cast("DirectoryDependency", dependency) constraint["path"] = dependency.path.as_posix() if dependency.develop: constraint["develop"] = True elif dependency.is_file(): dependency = cast("FileDependency", dependency) constraint["path"] = dependency.path.as_posix() elif dependency.is_url(): dependency = cast("URLDependency", dependency) constraint["url"] = dependency.url elif dependency.is_vcs(): dependency = cast("VCSDependency", dependency) constraint[dependency.vcs] = dependency.source if dependency.branch: constraint["branch"] = dependency.branch elif dependency.tag: constraint["tag"] = dependency.tag elif dependency.rev: constraint["rev"] = dependency.rev if dependency.directory: constraint["subdirectory"] = dependency.directory else: constraint["version"] = str(dependency.pretty_constraint) if dependency.extras: constraint["extras"] = sorted(dependency.extras) if dependency.is_optional(): constraint["optional"] = True if not dependency.marker.is_any(): constraint["markers"] = str(dependency.marker) dependencies[dependency.pretty_name].append(constraint) # All the constraints should have the same type, # but we want to simplify them if it's possible for dependency_name, constraints in dependencies.items(): if all( len(constraint) == 1 and "version" in constraint for constraint in constraints ): dependencies[dependency_name] = [ constraint["version"] for constraint in constraints ] data: dict[str, Any] = { "name": package.pretty_name, "version": package.pretty_version, "description": package.description or "", "optional": package.optional, "python-versions": package.python_versions, "groups": sorted(transitive_info.groups, key=group_sort_key), } if transitive_info.markers: if len(markers := set(transitive_info.markers.values())) == 1: if not (marker := next(iter(markers))).is_any(): data["markers"] = str(marker) else: data["markers"] = inline_table() for group, marker in sorted( transitive_info.markers.items(), key=lambda x: group_sort_key(x[0]), ): if not marker.is_any(): data["markers"][group] = str(marker) data["files"] = sorted( [ {k: v for k, v in f.items() if k in {"file", "hash"}} for f in package.files ], key=lambda x: x["file"], ) if dependencies: data["dependencies"] = table() for dep_name, constraints in dependencies.items(): if len(constraints) == 1: data["dependencies"][dep_name] = constraints[0] else: data["dependencies"][dep_name] = array().multiline(True) for constraint in constraints: data["dependencies"][dep_name].append(constraint) if package.extras: extras = {} for name, deps in sorted(package.extras.items()): extras[name] = sorted(dep.to_pep_508(with_extras=False) for dep in deps) data["extras"] = extras if package.source_url: url = package.source_url if package.source_type in ["file", "directory"]: # The lock file should only store paths relative to the root project url = Path( os.path.relpath( Path(url).resolve(), Path(self.lock.parent).resolve(), ) ).as_posix() data["source"] = {} if package.source_type: data["source"]["type"] = package.source_type data["source"]["url"] = url if package.source_reference: data["source"]["reference"] = package.source_reference if package.source_resolved_reference: data["source"]["resolved_reference"] = package.source_resolved_reference if package.source_subdirectory: data["source"]["subdirectory"] = package.source_subdirectory if package.source_type in ["directory", "git"]: data["develop"] = package.develop return data ================================================ FILE: src/poetry/packages/package_collection.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.packages.dependency_package import DependencyPackage if TYPE_CHECKING: from collections.abc import Iterable from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package class PackageCollection(list[DependencyPackage]): def __init__( self, dependency: Dependency, packages: Iterable[Package | DependencyPackage] = (), ) -> None: self._dependency = dependency super().__init__() for package in packages: self.append(package) def append(self, package: Package | DependencyPackage) -> None: if isinstance(package, DependencyPackage): package = package.package package = DependencyPackage(self._dependency, package) return super().append(package) ================================================ FILE: src/poetry/packages/transitive_package_info.py ================================================ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.version.markers import BaseMarker from poetry.core.version.markers import EmptyMarker if TYPE_CHECKING: from collections.abc import Iterable from packaging.utils import NormalizedName def group_sort_key(group: NormalizedName) -> tuple[bool, NormalizedName]: return group != MAIN_GROUP, group @dataclass class TransitivePackageInfo: depth: int # max depth in the dependency tree groups: set[NormalizedName] markers: dict[NormalizedName, BaseMarker] # group -> marker def get_marker(self, groups: Iterable[NormalizedName]) -> BaseMarker: marker: BaseMarker = EmptyMarker() for group in sorted(groups, key=group_sort_key): if group_marker := self.markers.get(group): marker = marker.union(group_marker) return marker ================================================ FILE: src/poetry/plugins/__init__.py ================================================ from __future__ import annotations from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin import Plugin __all__ = ["ApplicationPlugin", "Plugin"] ================================================ FILE: src/poetry/plugins/application_plugin.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.plugins.base_plugin import BasePlugin if TYPE_CHECKING: from poetry.console.application import Application from poetry.console.commands.command import Command class ApplicationPlugin(BasePlugin): """ Base class for application plugins. """ group = "poetry.application.plugin" @property def commands(self) -> list[type[Command]]: return [] def activate(self, application: Application) -> None: for command in self.commands: assert command.name is not None application.command_loader.register_factory(command.name, command) ================================================ FILE: src/poetry/plugins/base_plugin.py ================================================ from __future__ import annotations from abc import ABC from abc import abstractmethod class BasePlugin(ABC): """ Base class for all plugin types The `activate()` method must be implemented and receives the Poetry instance. """ PLUGIN_API_VERSION = "1.0.0" @property @abstractmethod def group(self) -> str: """ Name of entrypoint group the plugin belongs to. """ ================================================ FILE: src/poetry/plugins/plugin.py ================================================ from __future__ import annotations from abc import abstractmethod from typing import TYPE_CHECKING from poetry.plugins.base_plugin import BasePlugin if TYPE_CHECKING: from cleo.io.io import IO from poetry.poetry import Poetry class Plugin(BasePlugin): """ Generic plugin not related to the console application. """ group = "poetry.plugin" @abstractmethod def activate(self, poetry: Poetry, io: IO) -> None: ... ================================================ FILE: src/poetry/plugins/plugin_manager.py ================================================ from __future__ import annotations import hashlib import json import logging import shutil import sys from functools import cached_property from importlib import metadata from pathlib import Path from site import addsitedir from typing import TYPE_CHECKING import tomlkit from poetry.core.packages.project_package import ProjectPackage from poetry.__version__ import __version__ from poetry.installation import Installer from poetry.packages import Locker from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin import Plugin from poetry.repositories.installed_repository import InstalledRepository from poetry.toml import TOMLFile from poetry.utils._compat import tomllib from poetry.utils.env import Env from poetry.utils.env import EnvManager if TYPE_CHECKING: from collections.abc import Sequence from typing import Any from cleo.io.io import IO from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.poetry import Poetry logger = logging.getLogger(__name__) class PluginManager: """ This class registers and activates plugins. """ def __init__(self, group: str) -> None: self._group = group self._plugins: list[Plugin] = [] @staticmethod def add_project_plugin_path(directory: Path) -> None: from poetry.factory import Factory try: pyproject_toml = Factory.locate(directory) except RuntimeError: # no pyproject.toml -> no project plugins return plugin_path = pyproject_toml.parent / ProjectPluginCache.PATH if plugin_path.exists(): # insert at the beginning to allow overriding dependencies EnvManager.get_system_env(naive=True).sys_path.insert(0, str(plugin_path)) # process .pth files (among other things) addsitedir(str(plugin_path)) @classmethod def ensure_project_plugins(cls, poetry: Poetry, io: IO) -> None: ProjectPluginCache(poetry, io).ensure_plugins() def load_plugins(self) -> None: plugin_entrypoints = self.get_plugin_entry_points() for ep in plugin_entrypoints: self._load_plugin_entry_point(ep) def get_plugin_entry_points(self) -> list[metadata.EntryPoint]: return list(metadata.entry_points(group=self._group)) def activate(self, *args: Any, **kwargs: Any) -> None: for plugin in self._plugins: plugin.activate(*args, **kwargs) def _add_plugin(self, plugin: Plugin) -> None: if not isinstance(plugin, (Plugin, ApplicationPlugin)): raise ValueError( "The Poetry plugin must be an instance of Plugin or ApplicationPlugin" ) self._plugins.append(plugin) def _load_plugin_entry_point(self, ep: metadata.EntryPoint) -> None: logger.debug("Loading the %s plugin", ep.name) plugin = ep.load() if not issubclass(plugin, (Plugin, ApplicationPlugin)): raise ValueError( "The Poetry plugin must be an instance of Plugin or ApplicationPlugin" ) self._add_plugin(plugin()) class ProjectPluginCache: PATH = Path(".poetry") / "plugins" def __init__(self, poetry: Poetry, io: IO) -> None: self._poetry = poetry self._io = io self._path = poetry.pyproject_path.parent / self.PATH self._config_file = self._path / "config.toml" self._gitignore_file = self._path.parent / ".gitignore" @property def _plugin_section(self) -> dict[str, Any]: plugins = self._poetry.local_config.get("requires-plugins", {}) assert isinstance(plugins, dict) return plugins @cached_property def _config(self) -> dict[str, Any]: return { "python": sys.version, "poetry": __version__, "plugins-hash": hashlib.sha256( json.dumps(self._plugin_section, sort_keys=True).encode() ).hexdigest(), } def ensure_plugins(self) -> None: from poetry.factory import Factory # parse project plugins plugins = [] for name, constraints in self._plugin_section.items(): _constraints = ( constraints if isinstance(constraints, list) else [constraints] ) for _constraint in _constraints: plugins.append(Factory.create_dependency(name, _constraint)) if not plugins: if self._path.exists(): self._io.write_line( "No project plugins defined." " Removing the project's plugin cache" ) self._io.write_line("") shutil.rmtree(self._path) return if self._is_fresh(): if self._io.is_debug(): self._io.write_line("The project's plugin cache is up to date.") self._io.write_line("") return elif self._path.exists(): self._io.write_line( "Removing the project's plugin cache because it is outdated" ) # Just remove the cache for two reasons: # 1. Since the path of the cache has already been added to sys.path # at this point, we had to distinguish between packages installed # directly into Poetry's env and packages installed in the project cache. # 2. Updating packages in the cache does not work out of the box, # probably, because we use pip to uninstall and pip does not know # about the cache so that we end up with just overwriting installed # packages and multiple dist-info folders per package. # In sum, we keep it simple by always starting from an empty cache # if something has changed. shutil.rmtree(self._path) # determine plugins relevant for Poetry's environment poetry_env = EnvManager.get_system_env(naive=True) relevant_plugins = { plugin.name: plugin for plugin in plugins if plugin.marker.validate(poetry_env.marker_env) } if not relevant_plugins: if self._io.is_debug(): self._io.write_line( "No relevant project plugins for Poetry's environment defined." ) self._io.write_line("") self._write_config() return self._io.write_line( "Ensuring that the Poetry plugins required" " by the project are available..." ) # check if required plugins are already available missing_plugin_count = len(relevant_plugins) satisfied_plugins = set() insufficient_plugins = [] installed_packages = [] installed_repo = InstalledRepository.load(poetry_env) for package in installed_repo.packages: if required_plugin := relevant_plugins.get(package.name): if package.satisfies(required_plugin): satisfied_plugins.add(package.name) installed_packages.append(package) else: insufficient_plugins.append((package, required_plugin)) # Do not add the package to installed_packages so that # the solver does not consider it. missing_plugin_count -= 1 if missing_plugin_count == 0: break else: installed_packages.append(package) if missing_plugin_count == 0 and not insufficient_plugins: # all required plugins are installed and satisfy the requirements self._write_config() self._io.write_line( "All required plugins have already been installed" " in Poetry's environment." ) self._io.write_line("") return if insufficient_plugins and self._io.is_debug(): plugins_str = "\n".join( f" - {req}\n installed: {p}" for p, req in insufficient_plugins ) self._io.write_line( "The following Poetry plugins are required by the project" f" but are not satisfied by the installed versions:\n{plugins_str}" ) # install missing plugins missing_plugins = [ plugin for name, plugin in relevant_plugins.items() if name not in satisfied_plugins ] plugins_str = "\n".join(f" - {p}" for p in missing_plugins) self._io.write_line( "The following Poetry plugins are required by the project" f" but are not installed in Poetry's environment:\n{plugins_str}\n" f"Installing Poetry plugins only for the current project..." ) self._install(missing_plugins, poetry_env, installed_packages) self._io.write_line("") self._write_config() def _is_fresh(self) -> bool: if not self._config_file.exists(): return False with self._config_file.open("rb") as f: stored_config = tomllib.load(f) return stored_config == self._config def _install( self, plugins: Sequence[Dependency], poetry_env: Env, locked_packages: Sequence[Package], ) -> None: project = ProjectPackage(name="poetry-project-instance", version="0") project.python_versions = ".".join(str(v) for v in poetry_env.version_info[:3]) # consider all packages in Poetry's environment pinned for package in locked_packages: project.add_dependency(package.to_dependency()) # add missing plugin dependencies for dependency in plugins: project.add_dependency(dependency) # force new package to be installed in the project cache instead of Poetry's env poetry_env.set_paths(purelib=self._path, platlib=self._path) self._ensure_cache_directory() installer = Installer( self._io, poetry_env, project, Locker(self._path / "poetry.lock", {}), self._poetry.pool, self._poetry.config, # Build installed repository from locked packages so that plugins # that may be overwritten are not included. InstalledRepository(locked_packages), ) installer.update(True) if installer.run() != 0: raise RuntimeError("Failed to install required Poetry plugins") def _write_config(self) -> None: self._ensure_cache_directory() document = tomlkit.document() for key, value in self._config.items(): document[key] = value TOMLFile(self._config_file).write(data=document) def _ensure_cache_directory(self) -> None: if self._path.exists(): return self._path.mkdir(parents=True, exist_ok=True) # only write .gitignore if path did not exist before self._gitignore_file.write_text("*", encoding="utf-8") ================================================ FILE: src/poetry/poetry.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import cast from poetry.core.poetry import Poetry as BasePoetry from poetry.__version__ import __version__ from poetry.config.source import Source from poetry.pyproject.toml import PyProjectTOML if TYPE_CHECKING: from collections.abc import Mapping from pathlib import Path from packaging.utils import NormalizedName from poetry.core.packages.dependency import Dependency from poetry.core.packages.project_package import ProjectPackage from poetry.config.config import Config from poetry.packages.locker import Locker from poetry.plugins.plugin_manager import PluginManager from poetry.repositories.repository_pool import RepositoryPool from poetry.toml import TOMLFile class Poetry(BasePoetry): VERSION = __version__ def __init__( self, file: Path, local_config: dict[str, Any], package: ProjectPackage, locker: Locker, config: Config, disable_cache: bool = False, *, build_constraints: Mapping[NormalizedName, list[Dependency]] | None = None, ) -> None: from poetry.repositories.repository_pool import RepositoryPool super().__init__(file, local_config, package, pyproject_type=PyProjectTOML) self._locker = locker self._config = config self._pool = RepositoryPool(config=config) self._plugin_manager: PluginManager | None = None self._disable_cache = disable_cache self._build_constraints = build_constraints or {} @property def pyproject(self) -> PyProjectTOML: pyproject = super().pyproject return cast("PyProjectTOML", pyproject) @property def file(self) -> TOMLFile: return self.pyproject.file @property def locker(self) -> Locker: return self._locker @property def pool(self) -> RepositoryPool: return self._pool @property def config(self) -> Config: return self._config @property def disable_cache(self) -> bool: return self._disable_cache @property def build_constraints(self) -> Mapping[NormalizedName, list[Dependency]]: return self._build_constraints def set_locker(self, locker: Locker) -> Poetry: self._locker = locker return self def set_pool(self, pool: RepositoryPool) -> Poetry: self._pool = pool return self def set_config(self, config: Config) -> Poetry: self._config = config return self def get_sources(self) -> list[Source]: return [ Source(**source) for source in self.pyproject.data.get("tool", {}) .get("poetry", {}) .get("source", []) ] ================================================ FILE: src/poetry/publishing/__init__.py ================================================ from __future__ import annotations from poetry.publishing.publisher import Publisher __all__ = ["Publisher"] ================================================ FILE: src/poetry/publishing/hash_manager.py ================================================ from __future__ import annotations import hashlib import io from contextlib import suppress from typing import TYPE_CHECKING from typing import NamedTuple if TYPE_CHECKING: from pathlib import Path class Hexdigest(NamedTuple): md5: str | None sha256: str | None blake2_256: str | None class HashManager: def __init__(self) -> None: self._sha2_hasher = hashlib.sha256() self._md5_hasher = None with suppress(ValueError): # FIPS mode disables MD5 self._md5_hasher = hashlib.md5() self._blake_hasher = None with suppress(ValueError, TypeError): # FIPS mode disables blake2 self._blake_hasher = hashlib.blake2b(digest_size=256 // 8) def _md5_update(self, content: bytes) -> None: if self._md5_hasher is not None: self._md5_hasher.update(content) def _md5_hexdigest(self) -> str | None: if self._md5_hasher is not None: return self._md5_hasher.hexdigest() return None def _blake_update(self, content: bytes) -> None: if self._blake_hasher is not None: self._blake_hasher.update(content) def _blake_hexdigest(self) -> str | None: if self._blake_hasher is not None: return self._blake_hasher.hexdigest() return None def hash(self, file: Path) -> None: with file.open("rb") as fp: for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""): self._md5_update(content) self._sha2_hasher.update(content) self._blake_update(content) def hexdigest(self) -> Hexdigest: return Hexdigest( self._md5_hexdigest(), self._sha2_hasher.hexdigest(), self._blake_hexdigest(), ) ================================================ FILE: src/poetry/publishing/publisher.py ================================================ from __future__ import annotations import logging from typing import TYPE_CHECKING from poetry.publishing.uploader import Uploader from poetry.utils.authenticator import Authenticator if TYPE_CHECKING: from pathlib import Path from cleo.io.io import IO from poetry.poetry import Poetry logger = logging.getLogger(__name__) class Publisher: """ Registers and publishes packages to remote repositories. """ def __init__(self, poetry: Poetry, io: IO, dist_dir: Path | None = None) -> None: self._poetry = poetry self._package = poetry.package self._io = io self._uploader = Uploader(poetry, io, dist_dir) self._authenticator = Authenticator(poetry.config, self._io) @property def files(self) -> list[Path]: return self._uploader.files def publish( self, repository_name: str | None, username: str | None, password: str | None, cert: Path | None = None, client_cert: Path | None = None, dry_run: bool = False, skip_existing: bool = False, ) -> None: if not repository_name: url = "https://upload.pypi.org/legacy/" repository_name = "pypi" else: # Retrieving config information url = self._poetry.config.get(f"repositories.{repository_name}.url") if url is None: raise RuntimeError(f"Repository {repository_name} is not defined") if not (username and password): # Check if we have a token first token = self._authenticator.get_pypi_token(repository_name) if token: logger.debug("Found an API token for %s.", repository_name) username = "__token__" password = token else: auth = self._authenticator.get_http_auth(repository_name) if auth: logger.debug( "Found authentication information for %s.", repository_name ) username = auth.username password = auth.password certificates = self._authenticator.get_certs_for_repository(repository_name) resolved_cert = cert or certificates.cert or certificates.verify resolved_client_cert = client_cert or certificates.client_cert self._uploader.auth(username, password) if repository_name == "pypi": repository_name = "PyPI" self._io.write_line( f"Publishing {self._package.pretty_name}" f" ({self._uploader.version}) to" f" {repository_name}" ) self._uploader.upload( url, cert=resolved_cert, client_cert=resolved_client_cert, dry_run=dry_run, skip_existing=skip_existing, ) ================================================ FILE: src/poetry/publishing/uploader.py ================================================ from __future__ import annotations import itertools import tarfile import zipfile from collections import defaultdict from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Literal import requests from packaging.metadata import RawMetadata from packaging.metadata import parse_email from poetry.core.constraints.version import Version from poetry.core.masonry.utils.helpers import distribution_name from requests_toolbelt import user_agent from requests_toolbelt.multipart import MultipartEncoder from requests_toolbelt.multipart import MultipartEncoderMonitor from poetry.__version__ import __version__ from poetry.publishing.hash_manager import HashManager from poetry.utils.constants import REQUESTS_TIMEOUT from poetry.utils.patterns import wheel_file_re if TYPE_CHECKING: from cleo.io.io import IO from poetry.poetry import Poetry class UploadError(Exception): pass class Uploader: def __init__(self, poetry: Poetry, io: IO, dist_dir: Path | None = None) -> None: self._poetry = poetry self._dist_name = distribution_name(poetry.package.name) self._io = io self._dist_dir = dist_dir or self.default_dist_dir self._username: str | None = None self._password: str | None = None @property def user_agent(self) -> str: agent: str = user_agent("poetry", __version__) return agent @property def default_dist_dir(self) -> Path: return self._poetry.file.path.parent / "dist" @property def dist_dir(self) -> Path: if not self._dist_dir.is_absolute(): return self._poetry.file.path.parent / self._dist_dir return self._dist_dir @property def files(self) -> list[Path]: return self._files_and_version[0] @property def version(self) -> str: return self._files_and_version[1] @cached_property def _files_and_version(self) -> tuple[list[Path], str]: dist = self.dist_dir wheels = dist.glob(f"{self._dist_name}-*-*.whl") tars = dist.glob(f"{self._dist_name}-*.tar.gz") artifacts_by_version = defaultdict(list) for artifact in itertools.chain(wheels, tars): version = ( artifact.stem.removesuffix(".tar") .removeprefix(f"{self._dist_name}-") .split("-", maxsplit=1)[0] ) artifacts_by_version[version].append(artifact) match len(artifacts_by_version): case 0: return [], "" case 1: latest_version = next(iter(artifacts_by_version)) artifacts = artifacts_by_version[latest_version] case _: latest_version = max( artifacts_by_version, key=lambda v: Version.parse(v) ) artifacts = artifacts_by_version[latest_version] return sorted(artifacts, key=lambda a: (a.suffix == ".whl", a)), latest_version def auth(self, username: str | None, password: str | None) -> None: self._username = username self._password = password def make_session(self) -> requests.Session: session = requests.Session() auth = self.get_auth() if auth is not None: session.auth = auth session.headers["User-Agent"] = self.user_agent return session def get_auth(self) -> tuple[str, str] | None: if self._username is None or self._password is None: return None return (self._username, self._password) def upload( self, url: str, cert: Path | bool = True, client_cert: Path | None = None, dry_run: bool = False, skip_existing: bool = False, ) -> None: session = self.make_session() session.verify = str(cert) if isinstance(cert, Path) else cert if client_cert: session.cert = str(client_cert) with session: self._upload(session, url, dry_run, skip_existing) @classmethod def post_data(cls, file: Path) -> dict[str, Any]: file_type = cls._get_type(file) hash_manager = HashManager() hash_manager.hash(file) file_hashes = hash_manager.hexdigest() md5_digest = file_hashes.md5 sha2_digest = file_hashes.sha256 blake2_256_digest = file_hashes.blake2_256 py_version: str | None = None if file_type == "bdist_wheel": wheel_info = wheel_file_re.match(file.name) if wheel_info is not None: py_version = wheel_info.group("pyver") else: py_version = "source" data: dict[str, Any] = { # Upload API (https://docs.pypi.org/api/upload/) # ":action", "protocol_version" and "content are added later "md5_digest": md5_digest, "sha256_digest": sha2_digest, "blake2_256_digest": blake2_256_digest, "filetype": file_type, "pyversion": py_version, } for key, value in cls._get_metadata(file).items(): # strip trailing 's' to match API field names # see https://docs.pypi.org/api/upload/ if key in {"platforms", "supported_platforms", "license_files"}: key = key[:-1] # revert some special cases from packaging.metadata.parse_email() # "keywords" is not "multiple use" but a comma-separated string if key == "keywords": assert isinstance(value, list) value = ", ".join(value) # "project_urls" is not a dict if key == "project_urls": assert isinstance(value, dict) value = [f"{k}, {v}" for k, v in value.items()] data[key] = value return data def _upload( self, session: requests.Session, url: str, dry_run: bool = False, skip_existing: bool = False, ) -> None: for file in self.files: self._upload_file(session, url, file, dry_run, skip_existing) def _upload_file( self, session: requests.Session, url: str, file: Path, dry_run: bool = False, skip_existing: bool = False, ) -> None: from cleo.ui.progress_bar import ProgressBar if not file.is_file(): raise UploadError(f"Archive ({file}) does not exist") data = self.post_data(file) data.update({":action": "file_upload", "protocol_version": "1"}) data_to_send: list[tuple[str, Any]] = self._prepare_data(data) with file.open("rb") as fp: data_to_send.append( ("content", (file.name, fp, "application/octet-stream")) ) encoder = MultipartEncoder(data_to_send) bar = ProgressBar(self._io, max=encoder.len) bar.set_format(f" - Uploading {file.name} %percent%%") monitor = MultipartEncoderMonitor( encoder, lambda monitor: bar.set_progress(monitor.bytes_read) ) bar.start() resp = None try: if not dry_run: resp = session.post( url, data=monitor, allow_redirects=False, headers={"Content-Type": monitor.content_type}, timeout=REQUESTS_TIMEOUT, ) if resp is None or 200 <= resp.status_code < 300: bar.set_format( f" - Uploading {file.name} %percent%%" ) bar.finish() elif 300 <= resp.status_code < 400: if self._io.output.is_decorated(): self._io.overwrite( f" - Uploading {file.name} FAILED" ) raise UploadError( "Redirects are not supported. " "Is the URL missing a trailing slash?" ) elif resp.status_code == 400 and "was ever registered" in resp.text: self._register(session, url) resp.raise_for_status() elif skip_existing and self._is_file_exists_error(resp): bar.set_format( f" - Uploading {file.name} File exists." " Skipping" ) bar.display() else: resp.raise_for_status() except requests.RequestException as e: if self._io.output.is_decorated(): self._io.overwrite( f" - Uploading {file.name} FAILED" ) if e.response is not None: message = ( f"HTTP Error {e.response.status_code}: " f"{e.response.reason} | {e.response.content!r}" ) raise UploadError(message) from e raise UploadError("Error connecting to repository") from e finally: self._io.write_line("") def _register(self, session: requests.Session, url: str) -> requests.Response: """ Register a package to a repository. """ data = self.post_data(self.files[0]) data.update({":action": "submit", "protocol_version": "1"}) data_to_send = self._prepare_data(data) encoder = MultipartEncoder(data_to_send) resp = session.post( url, data=encoder, allow_redirects=False, headers={"Content-Type": encoder.content_type}, timeout=REQUESTS_TIMEOUT, ) resp.raise_for_status() return resp def _prepare_data(self, data: dict[str, Any]) -> list[tuple[str, str]]: data_to_send = [] for key, value in data.items(): if not isinstance(value, (list, tuple)): data_to_send.append((key, value)) else: for item in value: data_to_send.append((key, item)) return data_to_send @staticmethod def _get_type(file: Path) -> Literal["bdist_wheel", "sdist"]: exts = file.suffixes if exts and exts[-1] == ".whl": return "bdist_wheel" elif len(exts) >= 2 and "".join(exts[-2:]) == ".tar.gz": return "sdist" raise ValueError("Unknown distribution format " + "".join(exts)) @staticmethod def _get_metadata(file: Path) -> RawMetadata: if file.suffix == ".whl": with zipfile.ZipFile(file) as z: for name in z.namelist(): parts = Path(name).parts if ( len(parts) == 2 and parts[1] == "METADATA" and parts[0].endswith(".dist-info") ): with z.open(name) as mf: return parse_email(mf.read().decode("utf-8"))[0] raise FileNotFoundError("METADATA not found in wheel") elif file.suffixes[-2:] == [".tar", ".gz"]: with tarfile.open(file, "r:gz") as tar: for member in tar.getmembers(): parts = Path(member.name).parts if ( len(parts) == 2 and parts[1] == "PKG-INFO" and (pf := tar.extractfile(member)) ): return parse_email(pf.read().decode("utf-8"))[0] raise FileNotFoundError("PKG-INFO not found in sdist") raise ValueError(f"Unsupported file type: {file}") def _is_file_exists_error(self, response: requests.Response) -> bool: # based on https://github.com/pypa/twine/blob/a6dd69c79f7b5abfb79022092a5d3776a499e31b/twine/commands/upload.py#L32 status = response.status_code reason = response.reason.lower() text = response.text.lower() reason_and_text = reason + text return ( # pypiserver (https://pypi.org/project/pypiserver) status == 409 # PyPI / TestPyPI / GCP Artifact Registry or (status == 400 and "already exist" in reason_and_text) # Nexus Repository OSS (https://www.sonatype.com/nexus-repository-oss) or (status == 400 and "updating asset" in reason_and_text) or (status == 400 and "cannot be updated" in reason_and_text) # Artifactory (https://jfrog.com/artifactory/) or (status == 403 and "overwrite artifact" in reason_and_text) # Gitlab Enterprise Edition (https://about.gitlab.com) or (status == 400 and "already been taken" in reason_and_text) ) ================================================ FILE: src/poetry/puzzle/__init__.py ================================================ from __future__ import annotations from poetry.puzzle.solver import Solver __all__ = ["Solver"] ================================================ FILE: src/poetry/puzzle/exceptions.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.mixology.failure import SolveFailureError class SolverProblemError(Exception): def __init__(self, error: SolveFailureError) -> None: self._error = error super().__init__(str(error)) @property def error(self) -> SolveFailureError: return self._error class OverrideNeededError(Exception): def __init__(self, *overrides: dict[Package, dict[str, Dependency]]) -> None: self._overrides = overrides @property def overrides(self) -> tuple[dict[Package, dict[str, Dependency]], ...]: return self._overrides ================================================ FILE: src/poetry/puzzle/provider.py ================================================ from __future__ import annotations import functools import itertools import logging import re import time from collections import defaultdict from contextlib import contextmanager from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from typing import cast from cleo.ui.progress_indicator import ProgressIndicator from poetry.core.constraints.version import EmptyConstraint from poetry.core.constraints.version import Version from poetry.core.constraints.version import VersionRange from poetry.core.packages.utils.utils import get_python_constraint_from_marker from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import parse_marker from poetry.core.version.markers import union as marker_union from poetry.mixology.incompatibility import Incompatibility from poetry.mixology.incompatibility_cause import DependencyCauseError from poetry.mixology.incompatibility_cause import PythonCauseError from poetry.mixology.term import Term from poetry.packages import DependencyPackage from poetry.packages.direct_origin import DirectOrigin from poetry.packages.package_collection import PackageCollection from poetry.puzzle.exceptions import OverrideNeededError from poetry.repositories.repository_pool import Priority if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Collection from collections.abc import Iterable from collections.abc import Iterator from collections.abc import Sequence from pathlib import Path from cleo.io.io import IO from packaging.utils import NormalizedName from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.directory_dependency import DirectoryDependency from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.package import Package from poetry.core.packages.package import PackageFile from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.version.markers import BaseMarker from poetry.repositories import RepositoryPool from poetry.utils.env import Env logger = logging.getLogger(__name__) class IncompatibleConstraintsError(Exception): """ Exception when there are duplicate dependencies with incompatible constraints. """ def __init__( self, package: Package, *dependencies: Dependency, with_sources: bool = False ) -> None: constraints = [] for dep in dependencies: constraint = dep.to_pep_508() if dep.is_direct_origin(): # add version info because issue might be a version conflict # with a version constraint constraint += f" ({dep.constraint})" if with_sources and dep.source_name: constraint += f" ; source={dep.source_name}" constraints.append(constraint) super().__init__( f"Incompatible constraints in requirements of {package}:\n" + "\n".join(constraints) ) class Indicator(ProgressIndicator): CONTEXT: str | None = None @staticmethod @contextmanager def context() -> Iterator[Callable[[str | None], None]]: def _set_context(context: str | None) -> None: Indicator.CONTEXT = context yield _set_context _set_context(None) def _formatter_context(self) -> str: if Indicator.CONTEXT is None: return " " else: return f" {Indicator.CONTEXT} " def _formatter_elapsed(self) -> str: assert self._start_time is not None elapsed = time.time() - self._start_time return f"{elapsed:.1f}s" class Provider: UNSAFE_PACKAGES: ClassVar[set[str]] = set() def __init__( self, package: Package, pool: RepositoryPool, io: IO, *, locked: list[Package] | None = None, active_root_extras: Collection[NormalizedName] | None = None, ) -> None: self._package = package self._pool = pool self._direct_origin = DirectOrigin(self._pool.artifact_cache) self._io = io self._env: Env | None = None self._package_python_constraint = package.python_constraint self._is_debugging: bool = self._io.is_debug() or self._io.is_very_verbose() self._overrides: dict[Package, dict[str, Dependency]] = {} self._deferred_cache: dict[Dependency, Package] = {} self._load_deferred = True self._source_root: Path | None = None self._direct_origin_packages: dict[str, Package] = {} self._locked: dict[NormalizedName, list[DependencyPackage]] = defaultdict(list) self._use_latest: Collection[NormalizedName] = [] self._active_root_extras = ( frozenset(active_root_extras) if active_root_extras is not None else None ) self._explicit_sources: dict[str, str] = {} for package in locked or []: self._locked[package.name].append( DependencyPackage(package.to_dependency(), package) ) for dependency_packages in self._locked.values(): dependency_packages.sort( key=lambda p: p.package.version, reverse=True, ) self.get_package_from_pool = functools.cache(self._pool.package) self._refreshed: set[tuple[str, Version, str | None]] = set() @property def pool(self) -> RepositoryPool: return self._pool @property def use_latest(self) -> Collection[NormalizedName]: return self._use_latest @functools.cached_property def _overrides_marker_intersection(self) -> BaseMarker: overrides_marker_intersection: BaseMarker = AnyMarker() for dep_overrides in self._overrides.values(): for dep in dep_overrides.values(): overrides_marker_intersection = overrides_marker_intersection.intersect( dep.marker ) return overrides_marker_intersection @functools.cached_property def _python_constraint(self) -> VersionConstraint: return self._package_python_constraint.intersect( get_python_constraint_from_marker(self._overrides_marker_intersection) ) def is_debugging(self) -> bool: return self._is_debugging def set_overrides(self, overrides: dict[Package, dict[str, Dependency]]) -> None: self._overrides = overrides self.__dict__.pop("_python_constraint", None) self.__dict__.pop("_overrides_marker_intersection", None) def load_deferred(self, load_deferred: bool) -> None: self._load_deferred = load_deferred @contextmanager def use_source_root(self, source_root: Path) -> Iterator[Provider]: original_source_root = self._source_root self._source_root = source_root try: yield self finally: self._source_root = original_source_root @contextmanager def use_environment(self, env: Env) -> Iterator[Provider]: original_python_constraint = self._package_python_constraint self._env = env # We use the stable version here to improve support of environments of Python pre-release # versions, e.g. Python 3.14rc2. Without using the stable version here, a dependency with # a marker like `python_version >= "3.14"` would not be installed. self._package_python_constraint = Version.parse( env.marker_env["python_full_version"] ).stable try: yield self finally: self._env = None self._package_python_constraint = original_python_constraint @contextmanager def use_latest_for(self, names: Collection[NormalizedName]) -> Iterator[Provider]: self._use_latest = names try: yield self finally: self._use_latest = [] @staticmethod def validate_package_for_dependency( dependency: Dependency, package: Package ) -> None: if dependency.name != package.name: # For now, the dependency's name must match the actual package's name raise RuntimeError( f"The dependency name for {dependency.name} does not match the actual" f" package's name: {package.name}" ) def search_for_direct_origin_dependency(self, dependency: Dependency) -> Package: package = self._deferred_cache.get(dependency) if package is not None: pass elif dependency.is_vcs(): dependency = cast("VCSDependency", dependency) package = self._search_for_vcs(dependency) elif dependency.is_file(): dependency = cast("FileDependency", dependency) package = self._search_for_file(dependency) elif dependency.is_directory(): dependency = cast("DirectoryDependency", dependency) package = self._search_for_directory(dependency) elif dependency.is_url(): dependency = cast("URLDependency", dependency) package = self._search_for_url(dependency) else: raise RuntimeError( f"{dependency}: unknown direct dependency type {dependency.source_type}" ) if dependency.is_vcs(): dependency._source_reference = package.source_reference dependency._source_resolved_reference = package.source_resolved_reference dependency._source_subdirectory = package.source_subdirectory dependency._constraint = package.version dependency._pretty_constraint = package.version.text self._deferred_cache[dependency] = package return package def search_for(self, dependency: Dependency) -> list[DependencyPackage]: """ Search for the specifications that match the given dependency. The specifications in the returned list will be considered in reverse order, so the latest version ought to be last. """ if dependency.is_root: return PackageCollection(dependency, [self._package]) if dependency.is_direct_origin(): package = self.search_for_direct_origin_dependency(dependency) self._direct_origin_packages[dependency.name] = package return PackageCollection(dependency, [package]) # If we've previously found a direct-origin package that meets this dependency, # use it. # # We rely on the VersionSolver resolving direct-origin dependencies first. direct_origin_package = self._direct_origin_packages.get(dependency.name) if direct_origin_package and direct_origin_package.satisfies(dependency): packages = [direct_origin_package] return PackageCollection(dependency, packages) packages = self._pool.find_packages(dependency) packages.sort( key=lambda p: ( not p.yanked, not p.is_prerelease() and not dependency.allows_prereleases(), p.version, ), reverse=True, ) return PackageCollection(dependency, packages) def _search_for_vcs(self, dependency: VCSDependency) -> Package: """ Search for the specifications that match the given VCS dependency. Basically, we clone the repository in a temporary directory and get the information we need by checking out the specified reference. """ package = self._direct_origin.get_package_from_vcs( dependency.vcs, dependency.source, branch=dependency.branch, tag=dependency.tag, rev=dependency.rev, subdirectory=dependency.source_subdirectory, source_root=self._source_root or (self._env.path.joinpath("src") if self._env else None), ) self.validate_package_for_dependency(dependency=dependency, package=package) package.develop = dependency.develop return package def _search_for_file(self, dependency: FileDependency) -> Package: dependency.validate(raise_error=True) package = self._direct_origin.get_package_from_file(dependency.full_path) self.validate_package_for_dependency(dependency=dependency, package=package) if dependency.base is not None: package.root_dir = dependency.base return package def _search_for_directory(self, dependency: DirectoryDependency) -> Package: dependency.validate(raise_error=True) package = self._direct_origin.get_package_from_directory(dependency.full_path) self.validate_package_for_dependency(dependency=dependency, package=package) package.develop = dependency.develop if dependency.base is not None: package.root_dir = dependency.base return package def _search_for_url(self, dependency: URLDependency) -> Package: package = self._direct_origin.get_package_from_url(dependency.url) self.validate_package_for_dependency(dependency=dependency, package=package) for extra in sorted(dependency.extras): if extra in package.extras: for dep in package.extras[extra]: dep.activate() for extra_dep in package.extras[extra]: package.add_dependency(extra_dep) return package def _get_dependencies_with_overrides( self, dependencies: list[Dependency], package: Package ) -> list[Dependency]: overrides = self._overrides.get(package, {}) _dependencies = [] overridden = [] for dep in dependencies: if dep.name in overrides: if dep.name in overridden: continue # empty constraint is used in overrides to mark that the package has # already been handled and is not required for the attached markers if not overrides[dep.name].constraint.is_empty(): _dependencies.append(overrides[dep.name]) overridden.append(dep.name) continue _dependencies.append(dep) return _dependencies def incompatibilities_for( self, dependency_package: DependencyPackage ) -> list[Incompatibility]: """ Returns incompatibilities that encapsulate a given package's dependencies, or that it can't be safely selected. If multiple subsequent versions of this package have the same dependencies, this will return incompatibilities that reflect that. It won't return incompatibilities that have already been returned by a previous call to _incompatibilities_for(). """ package = dependency_package.package if package.is_root(): dependencies = package.all_requires else: dependencies = package.requires if not package.python_constraint.allows_all(self._python_constraint): transitive_python_constraint = get_python_constraint_from_marker( dependency_package.dependency.transitive_marker ) intersection = package.python_constraint.intersect( transitive_python_constraint ) difference = transitive_python_constraint.difference(intersection) # The difference is only relevant if it intersects # the root package python constraint difference = difference.intersect(self._python_constraint) if ( transitive_python_constraint.is_any() or self._python_constraint.intersect( dependency_package.dependency.python_constraint ).is_empty() or intersection.is_empty() or not difference.is_empty() ): return [ Incompatibility( [Term(package.to_dependency(), True)], PythonCauseError( package.python_versions, str(self._python_constraint) ), ) ] return [ Incompatibility( [Term(package.to_dependency(), True), Term(dep, False)], DependencyCauseError(), ) for dep in self._get_dependencies_with_overrides(dependencies, package) ] @staticmethod def _files_list_for_cmp(files: Sequence[PackageFile]) -> list[str]: """ :return: A list of strings representing the files and their hashes, for the purpose of comparing the file list to another one. We only use file+hash, because that's what uniquely identifies the file, the other properties (like URL) are not relevant. """ return sorted(f["file"] + f["hash"] for f in files) def complete_package( self, dependency_package: DependencyPackage ) -> DependencyPackage: package = dependency_package.package dependency = dependency_package.dependency if package.is_root(): dependency_package = dependency_package.clone() package = dependency_package.package dependency = dependency_package.dependency requires = package.all_requires elif package.is_direct_origin(): requires = package.requires else: if ( package.pretty_name, package.version, dependency.source_name, ) in self._refreshed: # circumvent lru_cache to avoid unnecessary refresh pool_package = self.pool.package( package.pretty_name, package.version, repository_name=dependency.source_name, ) else: pool_package = self.get_package_from_pool( package.pretty_name, package.version, repository_name=dependency.source_name, ) if package.files and self._files_list_for_cmp( package.files ) != self._files_list_for_cmp(pool_package.files): # This happens if additional artifacts are uploaded later. Either our own cache # is outdated or the lockfile has been created with an outdated cache. # Refresh to cover the first case. (It does not hurt much in the second case.) pool_package = self.pool.refresh(pool_package) self._refreshed.add( (package.pretty_name, package.version, dependency.source_name) ) dependency_package = DependencyPackage(dependency, pool_package) package = dependency_package.package dependency = dependency_package.dependency requires = package.requires found_extras = set() optional_dependencies = set() _dependencies = [] if dependency.extras: # Find all the optional dependencies that are wanted - taking care to allow # for self-referential extras. stack = sorted(dependency.extras) while stack: extra = stack.pop() if extra in found_extras: continue found_extras.add(extra) extra_dependencies = package.extras.get(extra, []) for extra_dependency in extra_dependencies: if extra_dependency.name == dependency.name: stack += sorted(extra_dependency.extras) else: optional_dependencies.add(extra_dependency.name) # If some extras/features were required, we need to add a special dependency # representing the base package to the current package. dependency_package = dependency_package.with_features(dependency.extras) package = dependency_package.package dependency = dependency_package.dependency new_dependency = package.without_features().to_dependency() new_dependency.marker = dependency.marker # When adding dependency foo[extra] -> foo, preserve foo's source, if it's # specified. This prevents us from trying to get foo from PyPI # when user explicitly set repo for foo[extra]. if not new_dependency.source_name and dependency.source_name: new_dependency.source_name = dependency.source_name _dependencies.append(new_dependency) for dep in requires: if not self._python_constraint.allows_any(dep.python_constraint): continue if dep.name in self.UNSAFE_PACKAGES: continue if self._env: marker_values = ( self._marker_values(self._active_root_extras) if package.is_root() else self._env.marker_env ) if not dep.marker.validate(marker_values): continue if not package.is_root() and ( (dep.is_optional() and dep.name not in optional_dependencies) or (dep.in_extras and not set(dep.in_extras).intersection(found_extras)) ): continue # For normal dependency resolution, we have to make sure that root extras # are represented in the markers. This is required to identify mutually # exclusive markers in cases like 'extra == "foo"' and 'extra != "foo"'. # However, for installation with re-resolving (installer.re-resolve=true, # which results in self._env being not None), this spoils the result # because we have to keep extras so that they are uninstalled # when calculating the operations of the transaction. if self._env is None and package.is_root() and dep.in_extras: # The clone is required for installation with re-resolving # without an existing lock file because the root package is used # once for solving and a second time for re-resolving for installation. dep = dep.clone() dep.marker = dep.marker.intersect( parse_marker( " or ".join(f'extra == "{extra}"' for extra in dep.in_extras) ) ) _dependencies.append(dep) if self._load_deferred: # Retrieving constraints for deferred dependencies for dep in _dependencies: if dep.is_direct_origin(): locked = self.get_locked(dep) # If lock file contains exactly the same URL and reference # (commit hash) of dependency as is requested, # do not analyze it again: nothing could have changed. if locked is not None and locked.package.is_same_package_as(dep): continue self.search_for_direct_origin_dependency(dep) dependencies = self._get_dependencies_with_overrides(_dependencies, package) # Searching for duplicate dependencies # # If the duplicate dependencies have the same constraint, # the requirements will be merged. # # For instance: # • enum34; python_version=="2.7" # • enum34; python_version=="3.3" # # will become: # • enum34; python_version=="2.7" or python_version=="3.3" # # If the duplicate dependencies have different constraints # we have to split the dependency graph. # # An example of this is: # • pypiwin32 (220); sys_platform == "win32" and python_version >= "3.6" # • pypiwin32 (219); sys_platform == "win32" and python_version < "3.6" duplicates: dict[str, list[Dependency]] = defaultdict(list) for dep in dependencies: duplicates[dep.name].append(dep) dependencies = [] for dep_name, deps in duplicates.items(): if len(deps) == 1: dependencies.append(deps[0]) continue self.debug(f"Duplicate dependencies for {dep_name}") # For dependency resolution, markers of duplicate dependencies must be # mutually exclusive. However, we have to take care about duplicates # with differing extras. duplicates_by_extras: dict[str, list[Dependency]] = defaultdict(list) for dep in deps: duplicates_by_extras[dep.complete_name].append(dep) if len(duplicates_by_extras) == 1: active_extras = ( self._active_root_extras if package.is_root() else found_extras ) deps = self._resolve_overlapping_markers(package, deps, active_extras) else: # There are duplicates with different extras. for complete_dep_name, deps_by_extra in duplicates_by_extras.items(): if len(deps_by_extra) > 1: duplicates_by_extras[complete_dep_name] = ( self._resolve_overlapping_markers( package, deps_by_extra, None ) ) if all(len(d) == 1 for d in duplicates_by_extras.values()) and all( d1[0].marker.intersect(d2[0].marker).is_empty() for d1, d2 in itertools.combinations( duplicates_by_extras.values(), 2 ) ): # Since all markers are mutually exclusive, # we can trigger overrides. deps = list(itertools.chain(*duplicates_by_extras.values())) else: # Too complicated to handle with overrides, # fallback to basic handling without overrides. for d in duplicates_by_extras.values(): dependencies.extend(d) continue if len(deps) == 1: self.debug(f"Merging requirements for {dep_name}") dependencies.append(deps[0]) continue # Sort out irrelevant requirements deps = [ dep for dep in deps if not self._overrides_marker_intersection.intersect( dep.marker ).is_empty() ] if len(deps) < 2: if not deps or (len(deps) == 1 and deps[0].constraint.is_empty()): msg = f"No relevant requirements for {dep_name}" else: msg = f"Only one relevant requirement for {dep_name}" dependencies.append(deps[0]) self.debug(msg) continue # At this point, we raise an exception that will # tell the solver to make new resolutions with specific overrides. # # For instance, if the foo (1.2.3) package has the following dependencies: # • bar (>=2.0) ; python_version >= "3.6" # • bar (<2.0) ; python_version < "3.6" # # then the solver will need to make two new resolutions # with the following overrides: # • {=2.0)>} # • {} def fmt_warning(d: Dependency) -> str: dependency_marker = d.marker if not d.marker.is_any() else "*" return ( f"{d.name} ({d.pretty_constraint})" f" with markers {dependency_marker}" ) warnings = ", ".join(fmt_warning(d) for d in deps[:-1]) warnings += f" and {fmt_warning(deps[-1])}" self.debug( f"Different requirements found for {warnings}." ) overrides = [] for dep in deps: current_overrides = self._overrides.copy() package_overrides = current_overrides.get(package, {}).copy() package_overrides.update({dep.name: dep}) current_overrides.update({package: package_overrides}) overrides.append(current_overrides) raise OverrideNeededError(*overrides) # Modifying dependencies as needed clean_dependencies = [] for dep in dependencies: if not dependency.transitive_marker.without_extras().is_any(): transitive_marker_intersection = ( dependency.transitive_marker.without_extras().intersect( dep.marker.without_extras() ) ) if transitive_marker_intersection.is_empty(): # The dependency is not needed, since the markers specified # for the current package selection are not compatible with # the markers for the current dependency, so we skip it continue dep.transitive_marker = transitive_marker_intersection if not dependency.python_constraint.is_any(): python_constraint_intersection = dep.python_constraint.intersect( dependency.python_constraint ) if python_constraint_intersection.is_empty(): # This dependency is not needed under current python constraint. continue clean_dependencies.append(dep) package = package.with_dependency_groups([], only=True) dependency_package = DependencyPackage(dependency, package) for dep in clean_dependencies: package.add_dependency(dep) if self._locked and package.is_root(): # At this point all duplicates have been eliminated via overrides # so that explicit sources are unambiguous. # Clear _explicit_sources because it might be filled # from a previous override. self._explicit_sources.clear() for dep in clean_dependencies: if dep.source_name: self._explicit_sources[dep.name] = dep.source_name return dependency_package def get_locked(self, dependency: Dependency) -> DependencyPackage | None: if dependency.name in self._use_latest: return None locked = self._locked.get(dependency.name, []) for dependency_package in locked: package = dependency_package.package if package.satisfies(dependency): if explicit_source := self._explicit_sources.get(dependency.name): dependency.source_name = explicit_source elif ( not dependency.source_name and package.source_type == "legacy" and package.source_reference and self._pool.get_priority(package.source_reference) == Priority.EXPLICIT ): continue return DependencyPackage(dependency, package) return None def debug(self, message: str, depth: int = 0) -> None: if not (self._io.is_very_verbose() or self._io.is_debug()): return if message.startswith("fact:"): if "depends on" in message: m = re.match(r"fact: (.+?) depends on (.+?) \((.+?)\)", message) if m is None: raise ValueError(f"Unable to parse fact: {message}") m2 = re.match(r"(.+?) \((.+?)\)", m.group(1)) if m2: name = m2.group(1) version = f" ({m2.group(2)})" else: name = m.group(1) version = "" message = ( f"fact: {name}{version} " f"depends on {m.group(2)} ({m.group(3)})" ) elif " is " in message: message = re.sub( "fact: (.+) is (.+)", "fact: \\1 is \\2", message, ) else: message = re.sub( r"(?<=: )(.+?) \((.+?)\)", "\\1 (\\2)", message ) message = f"fact: {message.split('fact: ')[1]}" elif message.startswith("selecting "): message = re.sub( r"selecting (.+?) \((.+?)\)", "selecting \\1 (\\2)", message, ) elif message.startswith("derived:"): m = re.match(r"derived: (.+?) \((.+?)\)$", message) if m: message = ( f"derived: {m.group(1)}" f" ({m.group(2)})" ) else: message = ( f"derived: {message.split('derived: ')[1]}" ) elif message.startswith("conflict:"): m = re.match(r"conflict: (.+?) depends on (.+?) \((.+?)\)", message) if m: m2 = re.match(r"(.+?) \((.+?)\)", m.group(1)) if m2: name = m2.group(1) version = f" ({m2.group(2)})" else: name = m.group(1) version = "" message = ( f"conflict: {name}{version} " f"depends on {m.group(2)} ({m.group(3)})" ) else: message = ( "conflict:" f" {message.split('conflict: ')[1]}" ) message = message.replace("! ", "! ") if self.is_debugging(): debug_info = str(message) debug_info = ( "\n".join( [ f"{str(depth).rjust(4)}: {s}" for s in debug_info.split("\n") ] ) + "\n" ) self._io.write(debug_info) def _group_by_source( self, dependencies: Iterable[Dependency] ) -> list[list[Dependency]]: """ Takes a list of dependencies and returns a list of groups of dependencies, each group containing all dependencies from the same source. """ groups: list[list[Dependency]] = [] for dep in dependencies: for group in groups: if ( dep.is_same_source_as(group[0]) and dep.source_name == group[0].source_name ): group.append(dep) break else: groups.append([dep]) return groups def _merge_dependencies_by_constraint( self, dependencies: Iterable[Dependency] ) -> list[Dependency]: """ Merge dependencies with the same constraint by building a union of their markers. For instance, if we have: - foo (>=2.0) ; python_version >= "3.6" and python_version < "3.7" - foo (>=2.0) ; python_version >= "3.7" we can avoid two overrides by merging them to: - foo (>=2.0) ; python_version >= "3.6" """ dep_groups = self._group_by_source(dependencies) merged_dependencies = [] for group in dep_groups: by_constraint: dict[VersionConstraint, list[Dependency]] = defaultdict(list) for dep in group: by_constraint[dep.constraint].append(dep) for deps in by_constraint.values(): dep = deps[0] if len(deps) > 1: new_markers = (dep.marker for dep in deps) dep.marker = marker_union(*new_markers) merged_dependencies.append(dep) return merged_dependencies def _is_relevant_marker( self, marker: BaseMarker, active_extras: Collection[NormalizedName] | None ) -> bool: """ A marker is relevant if - it is not empty - allowed by the project's python constraint - allowed by active extras of the dependency (not relevant for root package) - allowed by the environment (only during installation) """ return ( not marker.is_empty() and self._python_constraint.allows_any( get_python_constraint_from_marker(marker) ) and (active_extras is None or marker.validate({"extra": active_extras})) and (not self._env or marker.validate(self._env.marker_env)) ) def _resolve_overlapping_markers( self, package: Package, dependencies: list[Dependency], active_extras: Collection[NormalizedName] | None, ) -> list[Dependency]: """ Convert duplicate dependencies with potentially overlapping markers into duplicate dependencies with mutually exclusive markers. Therefore, the intersections of all combinations of markers and inverted markers have to be calculated. If such an intersection is relevant (not empty, etc.), the intersection of all constraints, whose markers were not inverted is built and a new dependency with the calculated version constraint and marker is added. (The marker of such a dependency does not overlap with the marker of any other new dependency.) """ # In order to reduce the number of intersections, # we merge duplicate dependencies by constraint. dependencies = self._merge_dependencies_by_constraint(dependencies) new_dependencies = [] for uses in itertools.product([True, False], repeat=len(dependencies)): # intersection of markers # For performance optimization, we don't just intersect all markers at once, # but intersect them one after the other to get empty markers early. # Further, we intersect the inverted markers at last because # they are more likely to overlap than the non-inverted ones. markers = ( dep.marker if use else dep.marker.invert() for use, dep in sorted( zip(uses, dependencies), key=lambda ud: ud[0], reverse=True ) ) used_marker_intersection: BaseMarker = AnyMarker() for m in markers: used_marker_intersection = used_marker_intersection.intersect(m) if not self._is_relevant_marker(used_marker_intersection, active_extras): continue # intersection of constraints constraint: VersionConstraint = VersionRange() specific_source_dependency = None used_dependencies = list(itertools.compress(dependencies, uses)) for dep in used_dependencies: if dep.is_direct_origin() or dep.source_name: # if direct origin or specific source: # conflict if specific source already set and not the same if specific_source_dependency and ( not dep.is_same_source_as(specific_source_dependency) or dep.source_name != specific_source_dependency.source_name ): raise IncompatibleConstraintsError( package, dep, specific_source_dependency, with_sources=True ) specific_source_dependency = dep constraint = constraint.intersect(dep.constraint) if constraint.is_empty(): # conflict in overlapping area raise IncompatibleConstraintsError(package, *used_dependencies) if not any(uses): # This is an edge case where the dependency is not required # for the resulting marker. However, we have to consider it anyway # in order to not miss other dependencies later, for instance: # • foo (1.0) ; python == 3.7 # • foo (2.0) ; python == 3.8 # • bar (2.0) ; python == 3.8 # • bar (3.0) ; python == 3.9 # the last dependency would be missed without this, # because the intersection with both foo dependencies is empty. # Set constraint to empty to mark dependency as "not required". constraint = EmptyConstraint() used_dependencies = dependencies # build new dependency with intersected constraint and marker # (and correct source) new_dep = ( specific_source_dependency if specific_source_dependency else used_dependencies[0] ).with_constraint(constraint) new_dep.marker = used_marker_intersection new_dependencies.append(new_dep) # In order to reduce the number of overrides we merge duplicate # dependencies by constraint again. After overlapping markers were # resolved, there might be new dependencies with the same constraint. return self._merge_dependencies_by_constraint(new_dependencies) def _marker_values( self, extras: Collection[NormalizedName] | None = None ) -> dict[str, Any]: """ Marker values, from `self._env` if present plus the supplied extras :param extras: the values to add to the 'extra' marker value """ result = dict(self._env.marker_env) if self._env is not None else {} if extras is not None: assert "extra" not in result, ( "'extra' marker key is already present in environment" ) result["extra"] = set(extras) return result ================================================ FILE: src/poetry/puzzle/solver.py ================================================ from __future__ import annotations import functools import time from collections import defaultdict from contextlib import contextmanager from typing import TYPE_CHECKING from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import EmptyMarker from poetry.core.version.markers import MultiMarker from poetry.core.version.markers import SingleMarker from poetry.core.version.markers import parse_marker from poetry.mixology import resolve_version from poetry.mixology.failure import SolveFailureError from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.puzzle.exceptions import OverrideNeededError from poetry.puzzle.exceptions import SolverProblemError from poetry.puzzle.provider import Indicator from poetry.puzzle.provider import Provider if TYPE_CHECKING: from collections.abc import Collection from collections.abc import Iterator from collections.abc import Sequence from cleo.io.io import IO from packaging.utils import NormalizedName from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.core.version.markers import BaseMarker from typing_extensions import Self from poetry.puzzle.transaction import Transaction from poetry.repositories import RepositoryPool from poetry.utils.env import Env # markers[child_package][parent_package][groups] -> BaseMarker MarkerOriginDict = defaultdict[ Package, defaultdict[Package, defaultdict[frozenset[NormalizedName], BaseMarker]], ] class Solver: def __init__( self, package: ProjectPackage, pool: RepositoryPool, installed: list[Package], locked: list[Package], io: IO, active_root_extras: Collection[NormalizedName] | None = None, ) -> None: self._package = package self._pool = pool self._installed_packages = installed self._locked_packages = locked self._io = io self._provider = Provider( self._package, self._pool, self._io, locked=locked, active_root_extras=active_root_extras, ) self._overrides: list[dict[Package, dict[str, Dependency]]] = [] @property def provider(self) -> Provider: return self._provider @contextmanager def use_environment(self, env: Env) -> Iterator[None]: with self.provider.use_environment(env): yield def solve( self, use_latest: Collection[NormalizedName] | None = None ) -> Transaction: from poetry.puzzle.transaction import Transaction with self._progress(), self._provider.use_latest_for(use_latest or []): start = time.time() packages = self._solve() # simplify markers by removing redundant information for transitive_info in packages.values(): for group, marker in transitive_info.markers.items(): transitive_info.markers[group] = simplify_marker( marker, self._package.python_constraint ) end = time.time() if len(self._overrides) > 1: self._provider.debug( # ignore the warning as provider does not do interpolation f"Complete version solving took {end - start:.3f}" f" seconds with {len(self._overrides)} overrides" ) self._provider.debug( # ignore the warning as provider does not do interpolation "Resolved with overrides:" f" {', '.join(f'({b})' for b in self._overrides)}" ) for p in packages: if p.yanked: message = ( f"The locked version {p.pretty_version} for {p.pretty_name} is a" " yanked version." ) if p.yanked_reason: message += f" Reason for being yanked: {p.yanked_reason}" self._io.write_error_line(f"Warning: {message}") return Transaction( self._locked_packages, packages, installed_packages=self._installed_packages, root_package=self._package, ) @contextmanager def _progress(self) -> Iterator[None]: if not self._io.output.is_decorated() or self._provider.is_debugging(): self._io.write_line("Resolving dependencies...") yield else: indicator = Indicator( self._io, "{message}{context}({elapsed:2s})" ) with indicator.auto( "Resolving dependencies...", "Resolving dependencies...", ): yield def _solve_in_compatibility_mode( self, overrides: tuple[dict[Package, dict[str, Dependency]], ...], ) -> dict[Package, TransitivePackageInfo]: override_packages: list[ tuple[ dict[Package, dict[str, Dependency]], dict[Package, TransitivePackageInfo], ] ] = [] for override in overrides: self._provider.debug( # ignore the warning as provider does not do interpolation "Retrying dependency resolution " f"with the following overrides ({override})." ) self._provider.set_overrides(override) new_packages = self._solve() override_packages.append((override, new_packages)) return merge_override_packages( override_packages, self._package.python_constraint ) def _solve(self) -> dict[Package, TransitivePackageInfo]: if self._provider._overrides: self._overrides.append(self._provider._overrides) try: result = resolve_version(self._package, self._provider) packages = result.packages except OverrideNeededError as e: return self._solve_in_compatibility_mode(e.overrides) except SolveFailureError as e: raise SolverProblemError(e) return self._aggregate_solved_packages(packages) def _aggregate_solved_packages( self, packages: list[Package] ) -> dict[Package, TransitivePackageInfo]: combined_nodes, markers = depth_first_search( PackageNode(self._package, packages) ) results = dict(aggregate_package_nodes(nodes) for nodes in combined_nodes) calculate_markers(results, markers) # Merging feature packages with base packages solved_packages = {} for package in packages: if package.features: for _package in packages: if ( not _package.features and _package.name == package.name and _package.version == package.version ): for dep in package.requires: # Prevent adding base package as a dependency to itself if _package.name == dep.name: continue # Avoid duplication. if any( _dep == dep and _dep.marker == dep.marker for _dep in _package.requires ): continue _package.add_dependency(dep) else: solved_packages[package] = results[package] return solved_packages DFSNodeID = tuple[str, frozenset[str], bool] class DFSNode: def __init__(self, id: DFSNodeID, name: str, base_name: str) -> None: self.id = id self.name = name self.base_name = base_name def reachable(self) -> Sequence[Self]: return [] def visit(self, parents: list[PackageNode]) -> None: pass def __str__(self) -> str: return str(self.id) def depth_first_search( source: PackageNode, ) -> tuple[list[list[PackageNode]], MarkerOriginDict]: back_edges: dict[DFSNodeID, list[PackageNode]] = defaultdict(list) markers: MarkerOriginDict = defaultdict( lambda: defaultdict(lambda: defaultdict(EmptyMarker)) ) visited: set[DFSNodeID] = set() topo_sorted_nodes: list[PackageNode] = [] dfs_visit(source, back_edges, visited, topo_sorted_nodes, markers) # Combine the nodes by name combined_nodes: dict[str, list[PackageNode]] = defaultdict(list) for node in topo_sorted_nodes: node.visit(back_edges[node.id]) combined_nodes[node.name].append(node) combined_topo_sorted_nodes: list[list[PackageNode]] = [ combined_nodes.pop(node.name) for node in topo_sorted_nodes if node.name in combined_nodes ] return combined_topo_sorted_nodes, markers def dfs_visit( node: PackageNode, back_edges: dict[DFSNodeID, list[PackageNode]], visited: set[DFSNodeID], sorted_nodes: list[PackageNode], markers: MarkerOriginDict, ) -> None: if node.id in visited: return visited.add(node.id) for out_neighbor in node.reachable(): back_edges[out_neighbor.id].append(node) groups = out_neighbor.groups prev_marker = markers[out_neighbor.package][node.package][groups] new_marker = ( out_neighbor.marker if node.package.is_root() else out_neighbor.marker.without_extras() ) markers[out_neighbor.package][node.package][groups] = prev_marker.union( new_marker ) dfs_visit(out_neighbor, back_edges, visited, sorted_nodes, markers) sorted_nodes.insert(0, node) class PackageNode(DFSNode): def __init__( self, package: Package, packages: list[Package], previous: PackageNode | None = None, dep: Dependency | None = None, marker: BaseMarker | None = None, ) -> None: self.package = package self.packages = packages self.dep = dep self.marker = marker or AnyMarker() self.depth = -1 if not previous: self.groups: frozenset[NormalizedName] = frozenset() self.optional = True elif dep: self.groups = dep.groups self.optional = dep.is_optional() else: raise ValueError("Both previous and dep must be passed") package_repr = repr(package) super().__init__( (package_repr, self.groups, self.optional), package_repr, package.name, ) def reachable(self) -> Sequence[PackageNode]: children: list[PackageNode] = [] for dependency in self.package.all_requires: for pkg in self.packages: if pkg.complete_name == dependency.complete_name and pkg.satisfies( dependency ): marker = dependency.marker if self.package.is_root() and dependency.in_extras: marker = marker.intersect( parse_marker( " or ".join( f'extra == "{extra}"' for extra in dependency.in_extras ) ) ) children.append( PackageNode( pkg, self.packages, self, self.dep or dependency, marker, ) ) return children def visit(self, parents: list[PackageNode]) -> None: # The root package, which has no parents, is defined as having depth -1 # So that the root package's top-level dependencies have depth 0. self.depth = 1 + max( [ parent.depth if parent.base_name != self.base_name else parent.depth - 1 for parent in parents ] + [-2] ) def aggregate_package_nodes( nodes: list[PackageNode], ) -> tuple[Package, TransitivePackageInfo]: package = nodes[0].package depth = max(node.depth for node in nodes) groups: set[NormalizedName] = set() for node in nodes: groups.update(node.groups) optional = all(node.optional for node in nodes) for node in nodes: node.depth = depth node.optional = optional package.optional = optional # TransitivePackageInfo.markers is updated later, # because the nodes of all packages have to be aggregated first. return package, TransitivePackageInfo(depth, groups, {}) def calculate_markers( packages: dict[Package, TransitivePackageInfo], markers: MarkerOriginDict ) -> None: # group packages by depth packages_by_depth: dict[int, list[Package]] = defaultdict(list) max_depth = -1 for package, info in packages.items(): max_depth = max(max_depth, info.depth) packages_by_depth[info.depth].append(package) # calculate markers from lowest to highest depth # (start with depth 0 because the root package has depth -1) has_incomplete_markers = True while has_incomplete_markers: has_incomplete_markers = False for depth in range(max_depth + 1): for package in packages_by_depth[depth]: transitive_info = packages[package] transitive_marker: dict[NormalizedName, BaseMarker] = { group: EmptyMarker() for group in transitive_info.groups } for parent, group_markers in markers[package].items(): parent_info = packages[parent] if parent_info.groups: # If parent has groups, we need to intersect its per-group # markers with each edge marker and union into child's groups. if parent_info.groups != set(parent_info.markers): # there is a cycle -> we need one more iteration has_incomplete_markers = True continue for group in parent_info.groups: for edge_marker in group_markers.values(): transitive_marker[group] = transitive_marker[ group ].union( parent_info.markers[group].intersect(edge_marker) ) else: # Parent is the root (no groups). Edge markers specify which # dependency groups the edge belongs to. We should only add # the edge marker to the corresponding child groups. for groups, edge_marker in group_markers.items(): assert groups, ( f"Package {package.name} at depth {depth} has no groups." f" All dependencies except for the root package at depth -1 must have groups" ) for group in transitive_info.groups: if group in groups: transitive_marker[group] = transitive_marker[ group ].union(edge_marker) transitive_info.markers = transitive_marker def merge_override_packages( override_packages: list[ tuple[ dict[Package, dict[str, Dependency]], dict[Package, TransitivePackageInfo] ] ], python_constraint: VersionConstraint, ) -> dict[Package, TransitivePackageInfo]: result: dict[Package, TransitivePackageInfo] = {} all_packages: dict[ Package, list[tuple[Package, TransitivePackageInfo, BaseMarker]] ] = {} for override, o_packages in override_packages: override_marker: BaseMarker = AnyMarker() for deps in override.values(): for dep in deps.values(): override_marker = override_marker.intersect(dep.marker.without_extras()) override_marker = simplify_marker(override_marker, python_constraint) for package, info in o_packages.items(): for group, marker in info.markers.items(): # `override_marker` is often a SingleMarker or a MultiMarker, # `marker` often is a MultiMarker that contains `override_marker`. # We can "remove" `override_marker` from `marker` # because we will do an intersection later anyway. # By removing it now, it is more likely that we hit # the performance shortcut instead of the fallback algorithm. info.markers[group] = remove_other_from_marker(marker, override_marker) all_packages.setdefault(package, []).append( (package, info, override_marker) ) for package_duplicates in all_packages.values(): base = package_duplicates[0] remaining = package_duplicates[1:] package = base[0] package_info = base[1] first_override_marker = base[2] result[package] = package_info package_info.depth = max(info.depth for _, info, _ in package_duplicates) package_info.groups = { g for _, info, _ in package_duplicates for g in info.groups } if all(info.markers == package_info.markers for _, info, _ in remaining): # performance shortcut: # if markers are the same for all overrides, # we can use less expensive marker operations override_marker = EmptyMarker() for _, _, marker in package_duplicates: override_marker = override_marker.union(marker) package_info.markers = { group: override_marker.intersect(marker) for group, marker in package_info.markers.items() } else: # fallback / general algorithm with performance issues for group, marker in package_info.markers.items(): package_info.markers[group] = first_override_marker.intersect(marker) for _, info, override_marker in remaining: for group, marker in info.markers.items(): package_info.markers[group] = package_info.markers.get( group, EmptyMarker() ).union(override_marker.intersect(marker)) for duplicate_package, _, _ in remaining: for dep in duplicate_package.requires: if dep not in package.requires: package.add_dependency(dep) return result def remove_other_from_marker(marker: BaseMarker, other: BaseMarker) -> BaseMarker: if isinstance(other, SingleMarker): other_markers: set[BaseMarker] = {other} elif isinstance(other, MultiMarker): other_markers = set(other.markers) else: return marker if isinstance(marker, MultiMarker) and other_markers.issubset(marker.markers): return MultiMarker.of(*(m for m in marker.markers if m not in other_markers)) return marker @functools.cache def simplify_marker( marker: BaseMarker, python_constraint: VersionConstraint ) -> BaseMarker: """ Remove constraints from markers that are covered by the projects Python constraint. Use cache because we call this function often for the same markers. """ return marker.reduce_by_python_constraint(python_constraint) ================================================ FILE: src/poetry/puzzle/transaction.py ================================================ from __future__ import annotations from collections import defaultdict from typing import TYPE_CHECKING from typing import Any from poetry.utils.extras import get_extra_package_names if TYPE_CHECKING: from collections.abc import Mapping from packaging.utils import NormalizedName from poetry.core.packages.package import Package from poetry.installation.operations.operation import Operation from poetry.packages.transitive_package_info import TransitivePackageInfo class Transaction: def __init__( self, current_packages: list[Package], result_packages: list[Package] | dict[Package, TransitivePackageInfo], installed_packages: list[Package] | None = None, root_package: Package | None = None, marker_env: Mapping[str, Any] | None = None, groups: set[NormalizedName] | None = None, ) -> None: self._current_packages = current_packages self._result_packages = result_packages if installed_packages is None: installed_packages = [] self._installed_packages = {pkg.name: pkg for pkg in installed_packages} self._root_package = root_package self._marker_env = marker_env self._groups = groups def get_solved_packages(self) -> dict[Package, TransitivePackageInfo]: assert isinstance(self._result_packages, dict) return self._result_packages def calculate_operations( self, *, with_uninstalls: bool = True, synchronize: bool = False, skip_directory: bool = False, extras: set[NormalizedName] | None = None, system_site_packages: set[NormalizedName] | None = None, ) -> list[Operation]: from poetry.installation.operations import Install from poetry.installation.operations import Uninstall from poetry.installation.operations import Update if not system_site_packages: system_site_packages = set() operations: list[Operation] = [] extra_packages: set[NormalizedName] = set() if self._marker_env: marker_env_with_extras = dict(self._marker_env) if extras is not None: marker_env_with_extras["extra"] = extras elif extras is not None: assert self._root_package is not None extra_packages = get_extra_package_names( self._result_packages, {k: [d.name for d in v] for k, v in self._root_package.extras.items()}, extras, ) if isinstance(self._result_packages, dict): priorities = { pkg: info.depth for pkg, info in self._result_packages.items() } else: priorities = defaultdict(int) relevant_result_packages: set[NormalizedName] = set() for result_package in self._result_packages: is_unsolicited_extra = False if self._marker_env: assert self._groups is not None assert isinstance(self._result_packages, dict) info = self._result_packages[result_package] if info.groups & self._groups and info.get_marker( self._groups ).validate(marker_env_with_extras): relevant_result_packages.add(result_package.name) elif result_package.optional: is_unsolicited_extra = True else: continue else: is_unsolicited_extra = extras is not None and ( result_package.optional and result_package.name not in extra_packages ) if not is_unsolicited_extra: relevant_result_packages.add(result_package.name) if installed_package := self._installed_packages.get(result_package.name): # Extras that were not requested are not relevant. if is_unsolicited_extra: pass # We have to perform an update if the version or another # attribute of the package has changed (source type, url, ref, ...). elif result_package.version != installed_package.version or ( ( # This has to be done because installed packages cannot # have type "legacy". If a package with type "legacy" # is installed, the installed package has no source_type. # Thus, if installed_package has no source_type and # the result_package has source_type "legacy" (negation of # the following condition), update must not be performed. # This quirk has the side effect that when switching # from PyPI to legacy (or vice versa), # no update is performed. installed_package.source_type or result_package.source_type != "legacy" ) and not result_package.is_same_package_as(installed_package) ): operations.append( Update( installed_package, result_package, priority=priorities[result_package], ) ) else: operations.append(Install(result_package).skip("Already installed")) elif not (skip_directory and result_package.source_type == "directory"): op = Install(result_package, priority=priorities[result_package]) if is_unsolicited_extra: op.skip("Not required") operations.append(op) if with_uninstalls: uninstalls: set[NormalizedName] = set() result_packages = {package.name for package in self._result_packages} for current_package in self._current_packages: if current_package.name not in (result_packages | uninstalls) and ( installed_package := self._installed_packages.get( current_package.name ) ): uninstalls.add(installed_package.name) if installed_package.name not in system_site_packages: operations.append(Uninstall(installed_package)) if synchronize: # We preserve pip when not managed by poetry, this is done to avoid # externally managed virtual environments causing unnecessary removals. preserved_package_names = {"pip"} - relevant_result_packages for installed_package in self._installed_packages.values(): if installed_package.name in uninstalls: continue if ( self._root_package and installed_package.name == self._root_package.name ): continue if installed_package.name in preserved_package_names: continue if installed_package.name not in relevant_result_packages: uninstalls.add(installed_package.name) if installed_package.name not in system_site_packages: operations.append(Uninstall(installed_package)) return sorted( operations, key=lambda o: ( -o.priority, o.package.name, o.package.version, ), ) ================================================ FILE: src/poetry/py.typed ================================================ ================================================ FILE: src/poetry/pyproject/__init__.py ================================================ ================================================ FILE: src/poetry/pyproject/toml.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.core.pyproject.toml import PyProjectTOML as BasePyProjectTOML from tomlkit.api import table from tomlkit.items import Table from tomlkit.toml_document import TOMLDocument from poetry.toml import TOMLFile if TYPE_CHECKING: from pathlib import Path class PyProjectTOML(BasePyProjectTOML): """ Enhanced version of poetry-core's PyProjectTOML which is capable of writing pyproject.toml The poetry-core class uses tomli to read the file, here we use tomlkit to preserve comments and formatting when writing. """ def __init__(self, path: Path) -> None: super().__init__(path) self._toml_file = TOMLFile(path=path) self._toml_document: TOMLDocument | None = None @property def file(self) -> TOMLFile: return self._toml_file @property def data(self) -> TOMLDocument: if self._toml_document is None: if not self.file.exists(): self._toml_document = TOMLDocument() else: self._toml_document = self.file.read() return self._toml_document def save(self) -> None: data = self.data if self._build_system is not None: if "build-system" not in data: data["build-system"] = table() build_system = data["build-system"] assert isinstance(build_system, Table) build_system["requires"] = self._build_system.requires build_system["build-backend"] = self._build_system.build_backend self.file.write(data=data) def reload(self) -> None: self._toml_document = None self._build_system = None ================================================ FILE: src/poetry/repositories/__init__.py ================================================ from __future__ import annotations from poetry.repositories.repository import Repository from poetry.repositories.repository_pool import RepositoryPool __all__ = ["Repository", "RepositoryPool"] ================================================ FILE: src/poetry/repositories/abstract_repository.py ================================================ from __future__ import annotations from abc import ABC from abc import abstractmethod from typing import TYPE_CHECKING if TYPE_CHECKING: from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package class AbstractRepository(ABC): def __init__(self, name: str) -> None: self._name = name @property def name(self) -> str: return self._name @abstractmethod def find_packages(self, dependency: Dependency) -> list[Package]: ... @abstractmethod def search(self, query: str | list[str]) -> list[Package]: ... @abstractmethod def package(self, name: str, version: Version) -> Package: ... ================================================ FILE: src/poetry/repositories/cached_repository.py ================================================ from __future__ import annotations from abc import ABC from abc import abstractmethod from typing import TYPE_CHECKING from typing import Any from packaging.utils import canonicalize_name from poetry.core.constraints.version import parse_constraint from poetry.config.config import Config from poetry.repositories.repository import Repository from poetry.utils.cache import FileCache if TYPE_CHECKING: from packaging.utils import NormalizedName from poetry.core.constraints.version import Version from poetry.core.packages.package import Package from poetry.inspection.info import PackageInfo class CachedRepository(Repository, ABC): CACHE_VERSION = parse_constraint("2.1.0") def __init__( self, name: str, *, disable_cache: bool = False, config: Config | None = None ) -> None: super().__init__(name) self._disable_cache = disable_cache self._cache_dir = (config or Config.create()).repository_cache_directory / name self._release_cache: FileCache[dict[str, Any]] = FileCache(path=self._cache_dir) @abstractmethod def _get_release_info( self, name: NormalizedName, version: Version ) -> dict[str, Any]: ... def get_release_info(self, name: NormalizedName, version: Version) -> PackageInfo: """ Return the release information given a package name and a version. The information is returned from the cache if it exists or retrieved from the remote server. """ from poetry.inspection.info import PackageInfo if self._disable_cache: return PackageInfo.load(self._get_release_info(name, version)) cached = self._release_cache.remember( f"{name}:{version}", lambda: self._get_release_info(name, version) ) cache_version = cached.get("_cache_version", "0.0.0") if parse_constraint(cache_version) != self.CACHE_VERSION: # The cache must be updated self._log( f"The cache for {name} {version} is outdated. Refreshing.", level="debug", ) cached = self._get_release_info(name, version) self._release_cache.put(f"{name}:{version}", cached) return PackageInfo.load(cached) def package(self, name: str, version: Version) -> Package: return self.get_release_info(canonicalize_name(name), version).to_package( name=name ) def forget(self, name: str, version: Version) -> None: self._release_cache.forget(f"{canonicalize_name(name)}:{version}") ================================================ FILE: src/poetry/repositories/exceptions.py ================================================ from __future__ import annotations class RepositoryError(Exception): pass class PackageNotFoundError(Exception): pass class InvalidSourceError(Exception): pass ================================================ FILE: src/poetry/repositories/http_repository.py ================================================ from __future__ import annotations import functools import hashlib from contextlib import contextmanager from contextlib import suppress from pathlib import Path from tempfile import TemporaryDirectory from typing import TYPE_CHECKING from typing import Any import requests import requests.adapters from packaging.metadata import parse_email from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency import Dependency from poetry.core.version.markers import parse_marker from poetry.config.config import Config from poetry.inspection.info import PackageInfo from poetry.inspection.lazy_wheel import LazyWheelUnsupportedError from poetry.inspection.lazy_wheel import metadata_from_wheel_url from poetry.repositories.cached_repository import CachedRepository from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.exceptions import RepositoryError from poetry.repositories.link_sources.html import HTMLPage from poetry.repositories.link_sources.json import SimpleJsonPage from poetry.utils.authenticator import Authenticator from poetry.utils.constants import REQUESTS_TIMEOUT from poetry.utils.helpers import HTTPRangeRequestSupportedError from poetry.utils.helpers import download_file from poetry.utils.helpers import get_highest_priority_hash_type from poetry.utils.patterns import wheel_file_re if TYPE_CHECKING: from collections.abc import Iterator from packaging.utils import NormalizedName from poetry.core.packages.package import PackageFile from poetry.core.packages.utils.link import Link from poetry.repositories.link_sources.base import LinkSource from poetry.utils.authenticator import RepositoryCertificateConfig class HTTPRepository(CachedRepository): def __init__( self, name: str, url: str, *, config: Config | None = None, disable_cache: bool = False, pool_size: int = requests.adapters.DEFAULT_POOLSIZE, ) -> None: super().__init__(name, disable_cache=disable_cache, config=config) self._url = url if config is None: config = Config.create() self._authenticator = Authenticator( config=config, cache_id=name, disable_cache=disable_cache, pool_size=pool_size, ) self._authenticator.add_repository(name, url) self.get_page = functools.lru_cache(maxsize=None)(self._get_page) self._lazy_wheel = config.get("solver.lazy-wheel", True) self._max_retries = config.get("requests.max-retries", 0) # We are tracking if a domain supports range requests or not to avoid # unnecessary requests. # ATTENTION: A domain might support range requests only for some files, so the # meaning is as follows: # - Domain not in dict: We don't know anything. # - True: The domain supports range requests for at least some files. # - False: The domain does not support range requests for the files we tried. self._supports_range_requests: dict[str, bool] = {} @property def session(self) -> Authenticator: return self._authenticator @property def url(self) -> str: return self._url @property def certificates(self) -> RepositoryCertificateConfig: return self._authenticator.get_certs_for_url(self.url) @property def authenticated_url(self) -> str: return self._authenticator.authenticated_url(url=self.url) def _download( self, url: str, dest: Path, *, raise_accepts_ranges: bool = False ) -> None: return download_file( url, dest, session=self.session, raise_accepts_ranges=raise_accepts_ranges, max_retries=self._max_retries, ) @contextmanager def _cached_or_downloaded_file( self, link: Link, *, raise_accepts_ranges: bool = False ) -> Iterator[Path]: self._log(f"Downloading: {link.url}", level="debug") with TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: filepath = Path(temp_dir) / link.filename self._download( link.url, filepath, raise_accepts_ranges=raise_accepts_ranges ) yield filepath def _get_info_from_wheel(self, link: Link) -> PackageInfo: from poetry.inspection.info import PackageInfo netloc = link.netloc # If "lazy-wheel" is enabled and the domain supports range requests # or we don't know yet, we try range requests. raise_accepts_ranges = self._lazy_wheel if self._lazy_wheel and self._supports_range_requests.get(netloc, True): try: package_info = PackageInfo.from_metadata( metadata_from_wheel_url(link.filename, link.url, self.session) ) except LazyWheelUnsupportedError as e: # Do not set to False if we already know that the domain supports # range requests for some URLs! self._log( f"Disabling lazy wheel support for {netloc}: {e}", level="debug", ) raise_accepts_ranges = False self._supports_range_requests.setdefault(netloc, False) else: self._supports_range_requests[netloc] = True return package_info try: with self._cached_or_downloaded_file( link, raise_accepts_ranges=raise_accepts_ranges ) as filepath: return PackageInfo.from_wheel(filepath) except HTTPRangeRequestSupportedError: # The domain did not support range requests for the first URL(s) we tried, # but supports it for some URLs (especially the current URL), # so we abort the download, update _supports_range_requests to try # range requests for all files and use it for the current URL. self._log( f"Abort downloading {link.url} because server supports range requests", level="debug", ) self._supports_range_requests[netloc] = True return self._get_info_from_wheel(link) def _get_info_from_sdist(self, link: Link) -> PackageInfo: from poetry.inspection.info import PackageInfo with self._cached_or_downloaded_file(link) as filepath: return PackageInfo.from_sdist(filepath) def _get_info_from_metadata(self, link: Link) -> PackageInfo | None: if link.has_metadata: try: assert link.metadata_url is not None response = self.session.get(link.metadata_url) if link.metadata_hashes and ( hash_name := get_highest_priority_hash_type( link.metadata_hashes, f"{link.filename}.metadata" ) ): metadata_hash = getattr(hashlib, hash_name)( response.content ).hexdigest() if metadata_hash != link.metadata_hashes[hash_name]: self._log( f"Metadata file hash ({metadata_hash}) does not match" f" expected hash ({link.metadata_hashes[hash_name]})." f" Metadata file for {link.filename} will be ignored.", level="warning", ) return None metadata, _ = parse_email(response.content) return PackageInfo.from_metadata(metadata) except requests.HTTPError: self._log( f"Failed to retrieve metadata at {link.metadata_url}", level="warning", ) return None def _get_info_from_links( self, links: list[Link], *, ignore_yanked: bool ) -> PackageInfo: # Sort links by distribution type wheels: list[Link] = [] sdists: list[Link] = [] for link in links: if link.yanked and ignore_yanked: # drop yanked files unless the entire release is yanked continue if link.is_wheel: wheels.append(link) elif link.filename.endswith( (".tar.gz", ".zip", ".bz2", ".xz", ".Z", ".tar") ): sdists.append(link) # Prefer to read data from wheels: this is faster and more reliable if wheels: # We ought just to be able to look at any of the available wheels to read # metadata, they all should give the same answer. # # In practice this hasn't always been true. # # Most of the code in here is to deal with cases such as isort 4.3.4 which # published separate python3 and python2 wheels with quite different # dependencies. We try to detect such cases and combine the data from the # two wheels into what ought to have been published in the first place... universal_wheel = None universal_python2_wheel = None universal_python3_wheel = None platform_specific_wheels = [] for wheel in wheels: m = wheel_file_re.match(wheel.filename) if not m: continue pyver = m.group("pyver") abi = m.group("abi") plat = m.group("plat") if abi == "none" and plat == "any": # Universal wheel if pyver == "py2.py3": # Any Python universal_wheel = wheel elif pyver == "py2": universal_python2_wheel = wheel else: universal_python3_wheel = wheel else: platform_specific_wheels.append(wheel) if universal_wheel is not None: return self._get_info_from_metadata( universal_wheel ) or self._get_info_from_wheel(universal_wheel) info = None if universal_python2_wheel and universal_python3_wheel: info = self._get_info_from_metadata( universal_python2_wheel ) or self._get_info_from_wheel(universal_python2_wheel) py3_info = self._get_info_from_metadata( universal_python3_wheel ) or self._get_info_from_wheel(universal_python3_wheel) if info.requires_python or py3_info.requires_python: info.requires_python = str( parse_constraint(info.requires_python or "^2.7").union( parse_constraint(py3_info.requires_python or "^3") ) ) if py3_info.requires_dist: if not info.requires_dist: info.requires_dist = py3_info.requires_dist return info py2_requires_dist = { Dependency.create_from_pep_508(r).to_pep_508() for r in info.requires_dist } py3_requires_dist = { Dependency.create_from_pep_508(r).to_pep_508() for r in py3_info.requires_dist } base_requires_dist = py2_requires_dist & py3_requires_dist py2_only_requires_dist = py2_requires_dist - py3_requires_dist py3_only_requires_dist = py3_requires_dist - py2_requires_dist # Normalizing requires_dist requires_dist = list(base_requires_dist) for requirement in py2_only_requires_dist: dep = Dependency.create_from_pep_508(requirement) dep.marker = dep.marker.intersect( parse_marker("python_version == '2.7'") ) requires_dist.append(dep.to_pep_508()) for requirement in py3_only_requires_dist: dep = Dependency.create_from_pep_508(requirement) dep.marker = dep.marker.intersect( parse_marker("python_version >= '3'") ) requires_dist.append(dep.to_pep_508()) info.requires_dist = sorted(set(requires_dist)) if info: return info # Prefer non platform specific wheels if universal_python3_wheel: return self._get_info_from_metadata( universal_python3_wheel ) or self._get_info_from_wheel(universal_python3_wheel) if universal_python2_wheel: return self._get_info_from_metadata( universal_python2_wheel ) or self._get_info_from_wheel(universal_python2_wheel) if platform_specific_wheels: first_wheel = platform_specific_wheels[0] return self._get_info_from_metadata( first_wheel ) or self._get_info_from_wheel(first_wheel) return self._get_info_from_metadata(sdists[0]) or self._get_info_from_sdist( sdists[0] ) def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]: if not links: raise PackageNotFoundError( f'No valid distribution links found for package: "{data.name}" version:' f' "{data.version}"' ) files: list[PackageFile] = [] for link in links: if link.yanked and not data.yanked: # drop yanked files unless the entire release is yanked continue file_hash: str | None for hash_name in ("sha512", "sha384", "sha256"): if hash_name in link.hashes: file_hash = f"{hash_name}:{link.hashes[hash_name]}" break else: file_hash = self.calculate_sha256(link) if file_hash is None and ( hash_type := get_highest_priority_hash_type(link.hashes, link.filename) ): file_hash = f"{hash_type}:{link.hashes[hash_type]}" if file_hash is None: # Is that even possible? # Before introducing this warning and ignoring the file, # null hashes would have been written to the lockfile, # which should have been failed in the Chooser at latest. self._log( f"Failed to determine hash of {link.url}. Skipping file.", level="warning", ) else: files.append( { "file": link.filename, "hash": file_hash, "url": link.url_without_fragment, } ) if link.size is not None: files[-1]["size"] = link.size if link.upload_time_isoformat is not None: files[-1]["upload_time"] = link.upload_time_isoformat if not files: raise PackageNotFoundError( f'Could not determine a hash for any distribution link of package: "{data.name}" version:' f' "{data.version}"' ) data.files = files # drop yanked files unless the entire release is yanked info = self._get_info_from_links(links, ignore_yanked=not data.yanked) data.summary = info.summary data.requires_dist = info.requires_dist data.requires_python = info.requires_python return data.asdict() def calculate_sha256(self, link: Link) -> str | None: with self._cached_or_downloaded_file(link) as filepath: hash_name = get_highest_priority_hash_type(link.hashes, link.filename) known_hash = None with suppress(ValueError, AttributeError): # Handle ValueError here as well since under FIPS environments # this is what is raised (e.g., for MD5) known_hash = getattr(hashlib, hash_name)() if hash_name else None required_hash = hashlib.sha256() chunksize = 4096 with filepath.open("rb") as f: while True: chunk = f.read(chunksize) if not chunk: break if known_hash: known_hash.update(chunk) required_hash.update(chunk) if ( not hash_name or not known_hash or known_hash.hexdigest() == link.hashes[hash_name] ): return f"{required_hash.name}:{required_hash.hexdigest()}" return None def _get_response( self, endpoint: str, *, headers: dict[str, str] | None = None ) -> requests.Response | None: url = self._url + endpoint try: response: requests.Response = self.session.get( url, raise_for_status=False, timeout=REQUESTS_TIMEOUT, headers=headers ) if response.status_code in (401, 403): self._log( f"Authorization error accessing {url}", level="warning", ) return None if response.status_code == 404: return None response.raise_for_status() except requests.exceptions.HTTPError as e: raise RepositoryError(e) if response.url != url: self._log( f"Response URL {response.url} differs from request URL {url}", level="debug", ) return response def _get_prefer_json_header(self) -> dict[str, str]: # Prefer json, but accept anything for backwards compatibility. # Although the more specific value should be preferred to the less specific one # according to https://developer.mozilla.org/en-US/docs/Glossary/Quality_values, # we add a quality value because some servers still prefer html without one. return {"Accept": "application/vnd.pypi.simple.v1+json, */*;q=0.1"} def _is_json_response(self, response: requests.Response) -> bool: return ( response.headers.get("Content-Type", "").split(";")[0].strip() == "application/vnd.pypi.simple.v1+json" ) def _get_page(self, name: NormalizedName) -> LinkSource: response = self._get_response( f"/{name}/", headers=self._get_prefer_json_header() ) if not response: raise PackageNotFoundError(f"Package [{name}] not found.") if self._is_json_response(response): return SimpleJsonPage(response.url, response.json()) return HTMLPage(response.url, response.text) ================================================ FILE: src/poetry/repositories/installed_repository.py ================================================ from __future__ import annotations import itertools import json import logging from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING from packaging.utils import canonicalize_name from poetry.core.packages.package import Package from poetry.core.packages.utils.utils import is_python_project from poetry.core.packages.utils.utils import url_to_path from poetry.core.utils.helpers import module_name from poetry.repositories.repository import Repository from poetry.utils._compat import getencoding from poetry.utils.env import VirtualEnv if TYPE_CHECKING: from collections.abc import Sequence from poetry.utils.env import Env logger = logging.getLogger(__name__) class InstalledRepository(Repository): def __init__(self, packages: Sequence[Package] | None = None) -> None: super().__init__("poetry-installed", packages) self.system_site_packages: list[Package] = [] def add_package(self, package: Package, *, is_system_site: bool = False) -> None: super().add_package(package) if is_system_site: self.system_site_packages.append(package) @classmethod def get_package_paths(cls, env: Env, name: str) -> set[Path]: """ Process a .pth file within the site-packages directories, and return any valid paths. We skip executable .pth files as there is no reliable means to do this without side-effects to current run-time. Mo check is made that the item refers to a directory rather than a file, however, in order to maintain backwards compatibility, we allow non-existing paths to be discovered. The latter behaviour is different to how Python's site-specific hook configuration works. Reference: https://docs.python.org/3.8/library/site.html :param env: The environment to search for the .pth file in. :param name: The name of the package to search .pth file for. :return: A `Set` of valid `Path` objects. """ paths = set() # we identify the candidate pth files to check, this is done so to handle cases # where the pth file for foo-bar might have been installed as either foo-bar.pth # or foo_bar.pth (expected) in either pure or platform lib directories. candidates = itertools.product( {env.purelib, env.platlib}, {name, module_name(name)}, ) for lib, module in candidates: pth_file = lib.joinpath(module).with_suffix(".pth") if not pth_file.exists(): continue with pth_file.open(encoding=getencoding()) as f: for line in f: line = line.strip() if line and not line.startswith(("#", "import ", "import\t")): path = Path(line) if not path.is_absolute(): path = lib.joinpath(path).resolve() paths.add(path) src_path = env.path / "src" / name if not paths and src_path.exists(): paths.add(src_path) return paths @classmethod def get_package_vcs_properties_from_path(cls, src: Path) -> tuple[str, str, str]: from poetry.vcs.git import Git info = Git.info(repo=src) return "git", info.origin, info.revision @classmethod def is_vcs_package(cls, package: Path | Package, env: Env) -> bool: # A VCS dependency should have been installed # in the src directory. src = env.path / "src" if isinstance(package, Package): return src.joinpath(package.name).is_dir() try: package.relative_to(env.path / "src") except ValueError: return False else: return True @classmethod def _create_package_from_distribution( cls, path: Path, dist_metadata: metadata.PackageMetadata, env: Env ) -> Package: # We first check for a direct_url.json file to determine # the type of package. if ( path.name.endswith(".dist-info") and path.joinpath("direct_url.json").exists() ): return cls._create_package_from_pep610(path, dist_metadata) is_standard_package = env.is_path_relative_to_lib(path) source_type = None source_url = None source_reference = None source_resolved_reference = None source_subdirectory = None if is_standard_package: if path.name.endswith(".dist-info"): paths = cls.get_package_paths(env=env, name=dist_metadata["name"]) if paths: is_editable_package = False for src in paths: if cls.is_vcs_package(src, env): ( source_type, source_url, source_reference, ) = cls.get_package_vcs_properties_from_path(src) break if not ( is_editable_package or env.is_path_relative_to_lib(src) ): is_editable_package = True else: # TODO: handle multiple source directories? if is_editable_package: source_type = "directory" path = paths.pop() if path.name == "src": path = path.parent source_url = path.as_posix() elif cls.is_vcs_package(path, env): ( source_type, source_url, source_reference, ) = cls.get_package_vcs_properties_from_path( env.path / "src" / canonicalize_name(dist_metadata["name"]) ) elif is_python_project(path.parent): source_type = "directory" source_url = str(path.parent) package = Package( dist_metadata["name"], dist_metadata["version"], source_type=source_type, source_url=source_url, source_reference=source_reference, source_resolved_reference=source_resolved_reference, source_subdirectory=source_subdirectory, ) package.description = dist_metadata.get( # type: ignore[attr-defined] "summary", "", ) return package @classmethod def _create_package_from_pep610( cls, path: Path, dist_metadata: metadata.PackageMetadata ) -> Package: source_type = None source_url = None source_reference = None source_resolved_reference = None source_subdirectory = None develop = False url_reference = json.loads( path.joinpath("direct_url.json").read_text(encoding="utf-8") ) if "archive_info" in url_reference: # File or URL distribution if url_reference["url"].startswith("file:"): # File distribution source_type = "file" source_url = url_to_path(url_reference["url"]).as_posix() else: # URL distribution source_type = "url" source_url = url_reference["url"] elif "dir_info" in url_reference: # Directory distribution source_type = "directory" source_url = url_to_path(url_reference["url"]).as_posix() develop = url_reference["dir_info"].get("editable", False) elif "vcs_info" in url_reference: # VCS distribution source_type = url_reference["vcs_info"]["vcs"] source_url = url_reference["url"] source_resolved_reference = url_reference["vcs_info"]["commit_id"] source_reference = url_reference["vcs_info"].get( "requested_revision", source_resolved_reference ) source_subdirectory = url_reference.get("subdirectory") package = Package( dist_metadata["name"], dist_metadata["version"], source_type=source_type, source_url=source_url, source_reference=source_reference, source_resolved_reference=source_resolved_reference, source_subdirectory=source_subdirectory, develop=develop, ) package.description = dist_metadata.get( # type: ignore[attr-defined] "summary", "", ) return package @classmethod def load(cls, env: Env, with_dependencies: bool = False) -> InstalledRepository: """ Load installed packages. """ from poetry.core.packages.dependency import Dependency repo = cls() seen = set() skipped = set() base_env = ( env.parent_env if isinstance(env, VirtualEnv) and env.includes_system_site_packages else None ) for entry in env.sys_path: if not entry.strip(): logger.debug( "Project environment contains an empty path in sys_path," " ignoring." ) continue for distribution in sorted( metadata.distributions(path=[entry]), key=lambda d: str(d._path), # type: ignore[attr-defined] ): path = Path(str(distribution._path)) # type: ignore[attr-defined] if path in skipped: continue dist_metadata = distribution.metadata # type: ignore[attr-defined] name = ( dist_metadata.get("name") # type: ignore[attr-defined] if dist_metadata else None ) if not dist_metadata or name is None: logger.warning( "Project environment contains an invalid distribution" " (%s). Consider removing it manually or recreate" " the environment.", path, ) skipped.add(path) continue name = canonicalize_name(name) if name in seen: continue package = cls._create_package_from_distribution( path, dist_metadata, env ) if with_dependencies: for require in dist_metadata.get_all("requires-dist", []): dep = Dependency.create_from_pep_508(require) package.add_dependency(dep) seen.add(package.name) repo.add_package( package, is_system_site=bool( base_env and base_env.is_path_relative_to_lib(path) ), ) return repo ================================================ FILE: src/poetry/repositories/legacy_repository.py ================================================ from __future__ import annotations from contextlib import suppress from functools import cached_property from typing import TYPE_CHECKING from typing import Any import requests.adapters from poetry.core.packages.package import Package from poetry.inspection.info import PackageInfo from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.http_repository import HTTPRepository from poetry.repositories.link_sources.base import SimpleRepositoryRootPage from poetry.repositories.link_sources.html import SimpleRepositoryHTMLRootPage from poetry.repositories.link_sources.json import SimpleRepositoryJsonRootPage if TYPE_CHECKING: from packaging.utils import NormalizedName from poetry.core.constraints.version import Version from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.utils.link import Link from poetry.config.config import Config class LegacyRepository(HTTPRepository): def __init__( self, name: str, url: str, *, config: Config | None = None, disable_cache: bool = False, pool_size: int = requests.adapters.DEFAULT_POOLSIZE, ) -> None: if name == "pypi": raise ValueError("The name [pypi] is reserved for repositories") super().__init__( name, url.rstrip("/"), config=config, disable_cache=disable_cache, pool_size=pool_size, ) def package(self, name: str, version: Version) -> Package: """ Retrieve the release information. This is a heavy task which takes time. We have to download a package to get the dependencies. We also need to download every file matching this release to get the various hashes. Note that this will be cached so the subsequent operations should be much faster. """ try: index = self._packages.index(Package(name, version)) return self._packages[index] except ValueError: package = super().package(name, version) package._source_type = "legacy" package._source_url = self._url package._source_reference = self.name return package def find_links_for_package(self, package: Package) -> list[Link]: try: page = self.get_page(package.name) except PackageNotFoundError: return [] return list(page.links_for_version(package.name, package.version)) def _find_packages( self, name: NormalizedName, constraint: VersionConstraint ) -> list[Package]: """ Find packages on the remote server. """ try: page = self.get_page(name) except PackageNotFoundError: self._log(f"No packages found for {name}", level="debug") return [] versions = [ (version, page.yanked(name, version)) for version in page.versions(name) if constraint.allows(version) ] return [ Package( name, version, source_type="legacy", source_reference=self.name, source_url=self._url, yanked=yanked, ) for version, yanked in versions ] def _get_release_info( self, name: NormalizedName, version: Version ) -> dict[str, Any]: page = self.get_page(name) links = list(page.links_for_version(name, version)) yanked = page.yanked(name, version) return self._links_to_data( links, PackageInfo( name=name, version=version.text, summary="", requires_dist=[], requires_python=None, files=[], yanked=yanked, cache_version=str(self.CACHE_VERSION), ), ) @cached_property def root_page(self) -> SimpleRepositoryRootPage: if not ( response := self._get_response("/", headers=self._get_prefer_json_header()) ): self._log( f"Unable to retrieve package listing from package source {self.name}", level="error", ) return SimpleRepositoryRootPage() if self._is_json_response(response): return SimpleRepositoryJsonRootPage(response.json()) return SimpleRepositoryHTMLRootPage(response.text) def search(self, query: str | list[str]) -> list[Package]: results: list[Package] = [] for candidate in self.root_page.search(query): with suppress(PackageNotFoundError): page = self.get_page(candidate) for package in page.packages: results.append(package) return results ================================================ FILE: src/poetry/repositories/link_sources/__init__.py ================================================ ================================================ FILE: src/poetry/repositories/link_sources/base.py ================================================ from __future__ import annotations import logging import re from functools import cached_property from typing import TYPE_CHECKING from typing import ClassVar from poetry.core.constraints.version import Version from poetry.core.packages.package import Package from poetry.core.version.exceptions import InvalidVersionError from poetry.utils.patterns import sdist_file_re from poetry.utils.patterns import wheel_file_re if TYPE_CHECKING: from collections import defaultdict from collections.abc import Iterator from packaging.utils import NormalizedName from poetry.core.packages.utils.link import Link LinkCache = defaultdict[NormalizedName, defaultdict[Version, list[Link]]] logger = logging.getLogger(__name__) class LinkSource: VERSION_REGEX = re.compile(r"(?i)([a-z0-9_\-.]+?)-(?=\d)([a-z0-9_.!+-]+)") CLEAN_REGEX = re.compile(r"[^a-z0-9$&+,/:;=?@.#%_\\|-]", re.I) SUPPORTED_FORMATS: ClassVar[list[str]] = [ ".tar.gz", ".whl", ".zip", ".tar.bz2", ".tar.xz", ".tar.Z", ".tar", ] def __init__(self, url: str) -> None: self._url = url @property def url(self) -> str: return self._url def versions(self, name: NormalizedName) -> Iterator[Version]: yield from self._link_cache[name] @property def packages(self) -> Iterator[Package]: for link in self.links: pkg = self.link_package_data(link) if pkg: yield pkg @property def links(self) -> Iterator[Link]: for links_per_version in self._link_cache.values(): for links in links_per_version.values(): yield from links @classmethod def link_package_data(cls, link: Link) -> Package | None: name: str | None = None version_string: str | None = None version: Version | None = None m = wheel_file_re.match(link.filename) or sdist_file_re.match(link.filename) if m: name = m.group("name") version_string = m.group("ver") else: info, _ext = link.splitext() match = cls.VERSION_REGEX.match(info) if match: name = match.group(1) version_string = match.group(2) if version_string: try: version = Version.parse(version_string) except InvalidVersionError: logger.debug( "Skipping url (%s) due to invalid version (%s)", link.url, version ) return None pkg = None if name and version: pkg = Package(name, version, source_url=link.url) return pkg def links_for_version( self, name: NormalizedName, version: Version ) -> Iterator[Link]: yield from self._link_cache[name][version] def clean_link(self, url: str) -> str: """Makes sure a link is fully encoded. That is, if a ' ' shows up in the link, it will be rewritten to %20 (while not over-quoting % or other characters).""" return self.CLEAN_REGEX.sub(lambda match: f"%{ord(match.group(0)):02x}", url) def yanked(self, name: NormalizedName, version: Version) -> str | bool: reasons = set() for link in self.links_for_version(name, version): if link.yanked: if link.yanked_reason: reasons.add(link.yanked_reason) else: # release is not yanked if at least one file is not yanked return False # if all files are yanked (or there are no files) the release is yanked if reasons: return "\n".join(sorted(reasons)) return True @cached_property def _link_cache(self) -> LinkCache: raise NotImplementedError() class SimpleRepositoryRootPage: """ This class represents the parsed content of a "simple" repository's root page. """ def search(self, query: str | list[str]) -> list[str]: results: list[str] = [] tokens = query if isinstance(query, list) else [query] for name in self.package_names: if any(token in name for token in tokens): results.append(name) return results @cached_property def package_names(self) -> list[str]: # should be overridden in subclasses return [] ================================================ FILE: src/poetry/repositories/link_sources/html.py ================================================ from __future__ import annotations import urllib.parse from collections import defaultdict from functools import cached_property from html import unescape from typing import TYPE_CHECKING from poetry.core.packages.utils.link import Link from poetry.repositories.link_sources.base import LinkSource from poetry.repositories.link_sources.base import SimpleRepositoryRootPage from poetry.repositories.parsers.html_page_parser import HTMLPageParser if TYPE_CHECKING: from poetry.repositories.link_sources.base import LinkCache class HTMLPage(LinkSource): def __init__(self, url: str, content: str) -> None: super().__init__(url=url) parser = HTMLPageParser() parser.feed(content) self._parsed = parser.anchors self._base_url: str | None = parser.base_url @cached_property def _link_cache(self) -> LinkCache: links: LinkCache = defaultdict(lambda: defaultdict(list)) for anchor in self._parsed: if href := anchor.get("href"): url = self.clean_link( urllib.parse.urljoin(self._base_url or self._url, href) ) pyrequire = anchor.get("data-requires-python") pyrequire = unescape(pyrequire) if pyrequire else None yanked_value = anchor.get("data-yanked") yanked: str | bool if yanked_value: yanked = unescape(yanked_value) else: yanked = "data-yanked" in anchor # see https://peps.python.org/pep-0714/#clients # and https://peps.python.org/pep-0658/#specification metadata: str | bool for metadata_key in ("data-core-metadata", "data-dist-info-metadata"): metadata_value = anchor.get(metadata_key) if metadata_value: metadata = unescape(metadata_value) else: metadata = metadata_key in anchor if metadata: break link = Link( url, requires_python=pyrequire, yanked=yanked, metadata=metadata ) if link.ext not in self.SUPPORTED_FORMATS: continue pkg = self.link_package_data(link) if pkg: links[pkg.name][pkg.version].append(link) return links class SimpleRepositoryHTMLRootPage(SimpleRepositoryRootPage): """ This class represents the parsed content of the HTML version of a "simple" repository's root page. This follows the specification laid out in PEP 503. See: https://peps.python.org/pep-0503/ """ def __init__(self, content: str | None = None) -> None: parser = HTMLPageParser() parser.feed(content or "") self._parsed = parser.anchors @cached_property def package_names(self) -> list[str]: results: list[str] = [] for anchor in self._parsed: if href := anchor.get("href"): results.append(href.rstrip("/")) return results ================================================ FILE: src/poetry/repositories/link_sources/json.py ================================================ from __future__ import annotations import urllib.parse from collections import defaultdict from functools import cached_property from typing import TYPE_CHECKING from typing import Any from poetry.core.packages.utils.link import Link from poetry.repositories.link_sources.base import LinkSource from poetry.repositories.link_sources.base import SimpleRepositoryRootPage if TYPE_CHECKING: from poetry.repositories.link_sources.base import LinkCache class SimpleJsonPage(LinkSource): """Links as returned by PEP 691 compatible JSON-based Simple API.""" def __init__(self, url: str, content: dict[str, Any]) -> None: super().__init__(url=url) self.content = content @cached_property def _link_cache(self) -> LinkCache: links: LinkCache = defaultdict(lambda: defaultdict(list)) for file in self.content["files"]: url = self.clean_link(urllib.parse.urljoin(self._url, file["url"])) requires_python = file.get("requires-python") hashes = file.get("hashes", {}) yanked = file.get("yanked", False) size = file.get("size") upload_time = file.get("upload-time") # see https://peps.python.org/pep-0714/#clients # and https://peps.python.org/pep-0691/#project-detail metadata: dict[str, str] | bool = False for metadata_key in ("core-metadata", "dist-info-metadata"): if metadata_key in file: metadata_value = file[metadata_key] if metadata_value and isinstance(metadata_value, dict): metadata = metadata_value else: metadata = bool(metadata_value) break link = Link( url, requires_python=requires_python, hashes=hashes, yanked=yanked, metadata=metadata, size=size, upload_time=upload_time, ) if link.ext not in self.SUPPORTED_FORMATS: continue pkg = self.link_package_data(link) if pkg: links[pkg.name][pkg.version].append(link) return links class SimpleRepositoryJsonRootPage(SimpleRepositoryRootPage): """ This class represents the parsed content of the JSON version of a "simple" repository's root page. This follows the specification laid out in PEP 691. See: https://peps.python.org/pep-0691/ """ def __init__(self, content: dict[str, Any]) -> None: self._content = content @cached_property def package_names(self) -> list[str]: results: list[str] = [] for project in self._content.get("projects", []): if name := project.get("name"): results.append(name) return results ================================================ FILE: src/poetry/repositories/lockfile_repository.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.repositories import Repository if TYPE_CHECKING: from poetry.core.packages.package import Package class LockfileRepository(Repository): """ Special repository that distinguishes packages not only by name and version, but also by source type, url, etc. """ def __init__(self) -> None: super().__init__("poetry-lockfile") def has_package(self, package: Package) -> bool: return any(p == package for p in self.packages) ================================================ FILE: src/poetry/repositories/parsers/__init__.py ================================================ ================================================ FILE: src/poetry/repositories/parsers/html_page_parser.py ================================================ from __future__ import annotations from html.parser import HTMLParser class HTMLPageParser(HTMLParser): def __init__(self) -> None: super().__init__() self.base_url: str | None = None self.anchors: list[dict[str, str | None]] = [] def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: if tag == "base" and self.base_url is None: base_url = dict(attrs).get("href") if base_url is not None: self.base_url = base_url elif tag == "a": self.anchors.append(dict(attrs)) ================================================ FILE: src/poetry/repositories/parsers/pypi_search_parser.py ================================================ from __future__ import annotations import functools from dataclasses import dataclass from html.parser import HTMLParser from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Callable # The following code was originally written for PDM project # https://github.com/pdm-project/pdm/blob/1f4f48a35cdded064def85df117bebf713f7c17a/src/pdm/models/search.py # and later changed to fit Poetry needs @dataclass class Result: name: str = "" version: str = "" description: str = "" class SearchResultParser(HTMLParser): """A simple HTML parser for pypi.org search results.""" def __init__(self) -> None: super().__init__() self.results: list[Result] = [] self._current: Result | None = None self._nest_anchors = 0 self._data_callback: Callable[[str], None] | None = None @staticmethod def _match_class(attrs: list[tuple[str, str | None]], name: str) -> bool: attrs_map = dict(attrs) return name in (attrs_map.get("class") or "").split() def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: if not self._current: if tag == "a" and self._match_class(attrs, "package-snippet"): self._current = Result() self._nest_anchors = 1 else: if tag == "span" and self._match_class(attrs, "package-snippet__name"): self._data_callback = functools.partial(setattr, self._current, "name") elif tag == "span" and self._match_class(attrs, "package-snippet__version"): self._data_callback = functools.partial( setattr, self._current, "version" ) elif tag == "p" and self._match_class( attrs, "package-snippet__description" ): self._data_callback = functools.partial( setattr, self._current, "description" ) elif tag == "a": self._nest_anchors += 1 def handle_data(self, data: str) -> None: if self._data_callback is not None: self._data_callback(data) self._data_callback = None def handle_endtag(self, tag: str) -> None: if tag != "a" or self._current is None: return self._nest_anchors -= 1 if self._nest_anchors == 0: if self._current.name and self._current.version: self.results.append(self._current) self._current = None ================================================ FILE: src/poetry/repositories/pypi_repository.py ================================================ from __future__ import annotations import contextlib import logging from typing import TYPE_CHECKING from typing import Any import requests import requests.adapters from cachecontrol.controller import logger as cache_control_logger from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link from poetry.core.version.exceptions import InvalidVersionError from poetry.core.version.requirements import InvalidRequirementError from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.http_repository import HTTPRepository from poetry.repositories.link_sources.json import SimpleJsonPage from poetry.repositories.parsers.pypi_search_parser import SearchResultParser from poetry.utils.constants import REQUESTS_TIMEOUT cache_control_logger.setLevel(logging.ERROR) logger = logging.getLogger(__name__) if TYPE_CHECKING: from packaging.utils import NormalizedName from poetry.core.constraints.version import Version from poetry.core.constraints.version import VersionConstraint from poetry.config.config import Config SUPPORTED_PACKAGE_TYPES = {"sdist", "bdist_wheel"} class PyPiRepository(HTTPRepository): def __init__( self, url: str = "https://pypi.org/", *, config: Config | None = None, disable_cache: bool = False, pool_size: int = requests.adapters.DEFAULT_POOLSIZE, fallback: bool = True, ) -> None: super().__init__( "PyPI", url.rstrip("/") + "/simple/", config=config, disable_cache=disable_cache, pool_size=pool_size, ) self._base_url = url self._fallback = fallback def search(self, query: str | list[str]) -> list[Package]: results = [] response = requests.get( self._base_url + "search", params={"q": query}, timeout=REQUESTS_TIMEOUT ) parser = SearchResultParser() parser.feed(response.text) for result in parser.results: try: package = Package(result.name, result.version) package.description = result.description.strip() results.append(package) except InvalidVersionError: self._log( f'Unable to parse version "{result.version}" for the' f" {result.name} package, skipping", level="debug", ) if not results: # in cases like PyPI search might not be available, we fallback to explicit searches # to allow for a nicer ux rather than finding nothing at all # see: https://discuss.python.org/t/fastly-interfering-with-pypi-search/73597/6 # tokens = query if isinstance(query, list) else [query] for token in tokens: with contextlib.suppress(InvalidRequirementError): results.extend( self.find_packages(Dependency.create_from_pep_508(token)) ) return results def get_package_info(self, name: NormalizedName) -> dict[str, Any]: """ Return the package information given its name. The information is returned from the cache if it exists or retrieved from the remote server. """ return self._get_package_info(name) def _find_packages( self, name: NormalizedName, constraint: VersionConstraint ) -> list[Package]: """ Find packages on the remote server. """ try: json_page = self.get_page(name) except PackageNotFoundError: self._log(f"No packages found for {name}", level="debug") return [] versions = [ (version, json_page.yanked(name, version)) for version in json_page.versions(name) if constraint.allows(version) ] return [Package(name, version, yanked=yanked) for version, yanked in versions] def _get_package_info(self, name: NormalizedName) -> dict[str, Any]: headers = {"Accept": "application/vnd.pypi.simple.v1+json"} info = self._get(f"simple/{name}/", headers=headers) if info is None: raise PackageNotFoundError(f"Package [{name}] not found.") return info def find_links_for_package(self, package: Package) -> list[Link]: json_data = self._get(f"pypi/{package.name}/{package.version}/json") if json_data is None: return [] links = [] for url in json_data["urls"]: if url["packagetype"] in SUPPORTED_PACKAGE_TYPES: h = f"sha256={url['digests']['sha256']}" links.append(Link(url["url"] + "#" + h, yanked=self._get_yanked(url))) return links def _get_release_info( self, name: NormalizedName, version: Version ) -> dict[str, Any]: from poetry.inspection.info import PackageInfo self._log(f"Getting info for {name} ({version}) from PyPI", "debug") json_data = self._get(f"pypi/{name}/{version}/json") if json_data is None: raise PackageNotFoundError(f"Package [{name}] not found.") info = json_data["info"] data = PackageInfo( name=info["name"], version=info["version"], summary=info["summary"], requires_dist=info["requires_dist"], requires_python=info["requires_python"], yanked=self._get_yanked(info), cache_version=str(self.CACHE_VERSION), ) try: version_info = json_data["urls"] except KeyError: version_info = [] files = info.get("files", []) for file_info in version_info: if file_info["packagetype"] in SUPPORTED_PACKAGE_TYPES: files.append( { "file": file_info["filename"], "hash": "sha256:" + file_info["digests"]["sha256"], "url": file_info["url"], } ) if (size := file_info.get("size")) is not None: files[-1]["size"] = size if upload_time := file_info.get("upload_time_iso_8601"): files[-1]["upload_time"] = upload_time data.files = files if self._fallback and data.requires_dist is None: self._log( "No dependencies found, downloading metadata and/or archives", level="debug", ) # No dependencies set (along with other information) # This might be due to actually no dependencies # or badly set metadata when uploading. # So, we need to make sure there is actually no # dependencies by introspecting packages. page = self.get_page(name) links = list(page.links_for_version(name, version)) info = self._get_info_from_links(links, ignore_yanked=not data.yanked) data.requires_dist = info.requires_dist if not data.requires_python: data.requires_python = info.requires_python return data.asdict() def _get_page(self, name: NormalizedName) -> SimpleJsonPage: source = self._base_url + f"simple/{name}/" info = self.get_package_info(name) return SimpleJsonPage(source, info) def _get( self, endpoint: str, headers: dict[str, str] | None = None ) -> dict[str, Any] | None: try: json_response = self.session.get( self._base_url + endpoint, raise_for_status=False, timeout=REQUESTS_TIMEOUT, headers=headers, ) except requests.exceptions.TooManyRedirects: # Cache control redirect loop. # We try to remove the cache and try again self.session.delete_cache(self._base_url + endpoint) json_response = self.session.get( self._base_url + endpoint, raise_for_status=False, timeout=REQUESTS_TIMEOUT, headers=headers, ) if json_response.status_code != 200: return None json: dict[str, Any] = json_response.json() return json @staticmethod def _get_yanked(json_data: dict[str, Any]) -> str | bool: if json_data.get("yanked", False): return json_data.get("yanked_reason") or True return False ================================================ FILE: src/poetry/repositories/repository.py ================================================ from __future__ import annotations import logging from typing import TYPE_CHECKING from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.repositories.abstract_repository import AbstractRepository from poetry.repositories.exceptions import PackageNotFoundError if TYPE_CHECKING: from collections.abc import Sequence from packaging.utils import NormalizedName from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link class Repository(AbstractRepository): def __init__(self, name: str, packages: Sequence[Package] | None = None) -> None: super().__init__(name) self._packages: list[Package] = [] for package in packages or []: self.add_package(package) @property def packages(self) -> list[Package]: return self._packages def find_packages(self, dependency: Dependency) -> list[Package]: packages = [] ignored_pre_release_packages = [] constraint = dependency.constraint allow_prereleases = dependency.allows_prereleases() for package in self._find_packages(dependency.name, constraint): if package.yanked and not isinstance(constraint, Version): # PEP 592: yanked files are always ignored, unless they are the only # file that matches a version specifier that "pins" to an exact # version continue if ( package.is_prerelease() and not allow_prereleases and not package.is_direct_origin() ): ignored_pre_release_packages.append(package) continue packages.append(package) self._log( f"{len(packages)} packages found for {dependency.name} {constraint!s}", level="debug", ) if allow_prereleases is False: # in contrast to None! return packages return packages or ignored_pre_release_packages def has_package(self, package: Package) -> bool: package_id = package.unique_name return any( package_id == repo_package.unique_name for repo_package in self.packages ) def add_package(self, package: Package) -> None: self._packages.append(package) def search(self, query: str | list[str]) -> list[Package]: results: list[Package] = [] tokens = query if isinstance(query, list) else [query] for package in self.packages: if any(token in package.name for token in tokens): results.append(package) return results def _find_packages( self, name: NormalizedName, constraint: VersionConstraint ) -> list[Package]: return [ package for package in self._packages if package.name == name and constraint.allows(package.version) ] def _log(self, msg: str, level: str = "info") -> None: logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") getattr(logger, level)(f"Source ({self.name}): {msg}") def __len__(self) -> int: return len(self._packages) def find_links_for_package(self, package: Package) -> list[Link]: return [] def package(self, name: str, version: Version) -> Package: canonicalized_name = canonicalize_name(name) for package in self.packages: if canonicalized_name == package.name and package.version == version: return package raise PackageNotFoundError(f"Package {name} ({version}) not found.") ================================================ FILE: src/poetry/repositories/repository_pool.py ================================================ from __future__ import annotations import enum from collections import OrderedDict from dataclasses import dataclass from enum import IntEnum from typing import TYPE_CHECKING from poetry.config.config import Config from poetry.repositories.abstract_repository import AbstractRepository from poetry.repositories.cached_repository import CachedRepository from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.repository import Repository from poetry.utils.cache import ArtifactCache if TYPE_CHECKING: from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package class Priority(IntEnum): # The order of the members below dictates the actual priority. The first member has # top priority. PRIMARY = enum.auto() SUPPLEMENTAL = enum.auto() EXPLICIT = enum.auto() @dataclass(frozen=True) class PrioritizedRepository: repository: Repository priority: Priority class RepositoryPool(AbstractRepository): def __init__( self, repositories: list[Repository] | None = None, *, config: Config | None = None, ) -> None: super().__init__("poetry-repository-pool") self._repositories: OrderedDict[str, PrioritizedRepository] = OrderedDict() if repositories is None: repositories = [] for repository in repositories: self.add_repository(repository) self._artifact_cache = ArtifactCache( cache_dir=(config or Config.create()).artifacts_cache_directory ) @staticmethod def from_packages(packages: list[Package], config: Config | None) -> RepositoryPool: pool = RepositoryPool(config=config) for package in packages: if package.is_direct_origin(): continue repo_name = package.source_reference or "PyPI" try: repo = pool.repository(repo_name) except IndexError: repo = Repository(repo_name) pool.add_repository(repo) if not repo.has_package(package): repo.add_package(package) return pool @property def repositories(self) -> list[Repository]: """ Returns the repositories in the pool, in the order they will be searched for packages. ATTENTION: For backwards compatibility and practical reasons, repositories with priority EXPLICIT are NOT included, because they will not be searched. """ sorted_repositories = self._sorted_repositories return [ prio_repo.repository for prio_repo in sorted_repositories if prio_repo.priority is not Priority.EXPLICIT ] @property def all_repositories(self) -> list[Repository]: return [prio_repo.repository for prio_repo in self._sorted_repositories] @property def _sorted_repositories(self) -> list[PrioritizedRepository]: return sorted( self._repositories.values(), key=lambda prio_repo: prio_repo.priority ) @property def artifact_cache(self) -> ArtifactCache: return self._artifact_cache def has_primary_repositories(self) -> bool: return self._contains_priority(Priority.PRIMARY) def _contains_priority(self, priority: Priority) -> bool: return any( prio_repo.priority is priority for prio_repo in self._repositories.values() ) def has_repository(self, name: str) -> bool: return name.lower() in self._repositories def repository(self, name: str) -> Repository: return self._get_prioritized_repository(name).repository def get_priority(self, name: str) -> Priority: return self._get_prioritized_repository(name).priority def _get_prioritized_repository(self, name: str) -> PrioritizedRepository: name = name.lower() if self.has_repository(name): return self._repositories[name] raise IndexError(f'Repository "{name}" does not exist.') def add_repository( self, repository: Repository, *, priority: Priority = Priority.PRIMARY ) -> RepositoryPool: """ Adds a repository to the pool. """ repository_name = repository.name.lower() if self.has_repository(repository_name): raise ValueError( f"A repository with name {repository_name} was already added." ) self._repositories[repository_name] = PrioritizedRepository( repository, priority ) return self def remove_repository(self, name: str) -> RepositoryPool: if not self.has_repository(name): raise IndexError( f"RepositoryPool can not remove unknown repository '{name}'." ) del self._repositories[name.lower()] return self def package( self, name: str, version: Version, repository_name: str | None = None ) -> Package: if repository_name: return self.repository(repository_name).package(name, version) for repo in self.repositories: try: return repo.package(name, version) except PackageNotFoundError: continue raise PackageNotFoundError(f"Package {name} ({version}) not found.") def find_packages(self, dependency: Dependency) -> list[Package]: repository_name = dependency.source_name if repository_name: return self.repository(repository_name).find_packages(dependency) packages: list[Package] = [] for repo in self.repositories: if packages and self.get_priority(repo.name) is Priority.SUPPLEMENTAL: break packages += repo.find_packages(dependency) return packages def search(self, query: str | list[str]) -> list[Package]: results: list[Package] = [] for repo in self.repositories: results += repo.search(query) return results def refresh(self, package: Package) -> Package: repository_name = package.source_reference or "PyPI" repo = self.repository(repository_name) if isinstance(repo, CachedRepository): repo.forget(package.name, package.version) return repo.package(package.name, package.version) ================================================ FILE: src/poetry/repositories/single_page_repository.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.link_sources.html import HTMLPage if TYPE_CHECKING: from packaging.utils import NormalizedName class SinglePageRepository(LegacyRepository): def _get_page(self, name: NormalizedName) -> HTMLPage: """ Single page repositories only have one page irrespective of endpoint. """ response = self._get_response("") if not response: raise PackageNotFoundError(f"Package [{name}] not found.") return HTMLPage(response.url, response.text) ================================================ FILE: src/poetry/toml/__init__.py ================================================ from __future__ import annotations from poetry.toml.exceptions import TOMLError from poetry.toml.file import TOMLFile __all__ = ["TOMLError", "TOMLFile"] ================================================ FILE: src/poetry/toml/exceptions.py ================================================ from __future__ import annotations from poetry.core.exceptions import PoetryCoreError from tomlkit.exceptions import TOMLKitError class TOMLError(TOMLKitError, PoetryCoreError): pass ================================================ FILE: src/poetry/toml/file.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from tomlkit.toml_file import TOMLFile as BaseTOMLFile if TYPE_CHECKING: from pathlib import Path from tomlkit.toml_document import TOMLDocument class TOMLFile(BaseTOMLFile): def __init__(self, path: Path) -> None: super().__init__(path) self.__path = path @property def path(self) -> Path: return self.__path def exists(self) -> bool: return self.__path.exists() def read(self) -> TOMLDocument: from tomlkit.exceptions import TOMLKitError from poetry.toml import TOMLError try: return super().read() except (ValueError, TOMLKitError) as e: raise TOMLError(f"Invalid TOML file {self.path.as_posix()}: {e}") def __str__(self) -> str: return self.__path.as_posix() ================================================ FILE: src/poetry/utils/__init__.py ================================================ ================================================ FILE: src/poetry/utils/_compat.py ================================================ from __future__ import annotations import locale import sys import warnings from contextlib import suppress if sys.version_info < (3, 11): # compatibility for python <3.11 import tomli as tomllib else: import tomllib from importlib import metadata as _metadata WINDOWS = sys.platform == "win32" def decode(string: bytes | str, encodings: list[str] | None = None) -> str: if not isinstance(string, bytes): return string encodings = encodings or ["utf-8", "latin1", "ascii"] for encoding in encodings: with suppress(UnicodeEncodeError, UnicodeDecodeError): return string.decode(encoding) return string.decode(encodings[0], errors="ignore") def encode(string: str, encodings: list[str] | None = None) -> bytes: if isinstance(string, bytes): return string encodings = encodings or ["utf-8", "latin1", "ascii"] for encoding in encodings: with suppress(UnicodeEncodeError, UnicodeDecodeError): return string.encode(encoding) return string.encode(encodings[0], errors="ignore") def getencoding() -> str: if sys.version_info < (3, 11): return locale.getpreferredencoding() else: return locale.getencoding() def __getattr__(name: str) -> object: if name == "metadata": warnings.warn( "Importing `metadata` from `poetry.utils._compat` is deprecated;" " use `importlib.metadata` directly.", DeprecationWarning, stacklevel=2, ) return _metadata raise AttributeError __all__ = [ "WINDOWS", "decode", "encode", "getencoding", "tomllib", ] ================================================ FILE: src/poetry/utils/authenticator.py ================================================ from __future__ import annotations import dataclasses import functools import logging import time import urllib.parse from os.path import commonprefix from pathlib import Path from typing import TYPE_CHECKING from typing import Any import requests import requests.adapters import requests.auth import requests.exceptions from cachecontrol import CacheControlAdapter from cachecontrol.caches import FileCache from requests_toolbelt import user_agent from poetry.__version__ import __version__ from poetry.config.config import Config from poetry.console.exceptions import ConsoleMessage from poetry.console.exceptions import PoetryRuntimeError from poetry.exceptions import PoetryError from poetry.utils.constants import REQUESTS_TIMEOUT from poetry.utils.constants import RETRY_AFTER_HEADER from poetry.utils.constants import STATUS_FORCELIST from poetry.utils.password_manager import HTTPAuthCredential from poetry.utils.password_manager import PasswordManager if TYPE_CHECKING: from cleo.io.io import IO logger = logging.getLogger(__name__) @dataclasses.dataclass(frozen=True) class RepositoryCertificateConfig: cert: Path | None = dataclasses.field(default=None) client_cert: Path | None = dataclasses.field(default=None) verify: bool = dataclasses.field(default=True) @classmethod def create( cls, repository: str, config: Config | None ) -> RepositoryCertificateConfig: config = config if config else Config.create() verify: str | bool = config.get( f"certificates.{repository}.verify", config.get(f"certificates.{repository}.cert", True), ) client_cert: str = config.get(f"certificates.{repository}.client-cert") return cls( cert=Path(verify) if isinstance(verify, str) else None, client_cert=Path(client_cert) if client_cert else None, verify=verify if isinstance(verify, bool) else True, ) @dataclasses.dataclass class AuthenticatorRepositoryConfig: name: str url: str netloc: str = dataclasses.field(init=False) path: str = dataclasses.field(init=False) def __post_init__(self) -> None: parsed_url = urllib.parse.urlsplit(self.url) self.netloc = parsed_url.netloc self.path = parsed_url.path def certs(self, config: Config) -> RepositoryCertificateConfig: return RepositoryCertificateConfig.create(self.name, config) def get_http_credentials( self, password_manager: PasswordManager ) -> HTTPAuthCredential: # try with the repository name via the password manager credential = password_manager.get_http_auth(self.name) if credential.password is not None: return credential if password_manager.use_keyring: # fallback to url and netloc based keyring entries credential = password_manager.get_credential( self.url, self.netloc, username=credential.username ) return credential class Authenticator: def __init__( self, config: Config | None = None, io: IO | None = None, cache_id: str | None = None, disable_cache: bool = False, pool_size: int = requests.adapters.DEFAULT_POOLSIZE, ) -> None: self._config = config or Config.create() self._io = io self._sessions_for_netloc: dict[str, requests.Session] = {} self._credentials: dict[str, HTTPAuthCredential] = {} self._certs: dict[str, RepositoryCertificateConfig] = {} self._configured_repositories: ( dict[str, AuthenticatorRepositoryConfig] | None ) = None self._password_manager = PasswordManager(self._config) self._cache_control = ( FileCache( self._config.repository_cache_directory / (cache_id or "_default_cache") / "_http" ) if not disable_cache else None ) self.get_repository_config_for_url = functools.lru_cache(maxsize=None)( self._get_repository_config_for_url ) self._pool_size = pool_size self._user_agent = user_agent("poetry", __version__) def create_session(self) -> requests.Session: session = requests.Session() session.headers["User-Agent"] = self._user_agent if self._cache_control is None: return session adapter = CacheControlAdapter( cache=self._cache_control, pool_maxsize=self._pool_size, ) session.mount("http://", adapter) session.mount("https://", adapter) return session def get_session(self, url: str | None = None) -> requests.Session: if not url: return self.create_session() parsed_url = urllib.parse.urlsplit(url) netloc = parsed_url.netloc if netloc not in self._sessions_for_netloc: logger.debug("Creating new session for %s", netloc) self._sessions_for_netloc[netloc] = self.create_session() return self._sessions_for_netloc[netloc] def close(self) -> None: for session in self._sessions_for_netloc.values(): if session is not None: session.close() def __del__(self) -> None: self.close() def delete_cache(self, url: str) -> None: if self._cache_control is not None: self._cache_control.delete(key=url) def authenticated_url(self, url: str) -> str: parsed = urllib.parse.urlparse(url) credential = self.get_credentials_for_url(url) if credential.username is not None and credential.password is not None: username = urllib.parse.quote(credential.username, safe="") password = urllib.parse.quote(credential.password, safe="") return ( f"{parsed.scheme}://{username}:{password}@{parsed.netloc}{parsed.path}" ) return url def request( self, method: str, url: str, raise_for_status: bool = True, **kwargs: Any ) -> requests.Response: headers = kwargs.get("headers") request = requests.Request(method, url, headers=headers) credential = self.get_credentials_for_url(url) if credential.username is not None or credential.password is not None: request = requests.auth.HTTPBasicAuth( credential.username or "", credential.password or "" )(request) session = self.get_session(url=url) prepared_request = session.prepare_request(request) proxies: dict[str, str] = kwargs.get("proxies", {}) stream: bool | None = kwargs.get("stream") certs = self.get_certs_for_url(url) verify: bool | str | Path = kwargs.get("verify") or certs.cert or certs.verify cert: str | Path | None = kwargs.get("cert") or certs.client_cert if cert is not None: cert = str(cert) verify = str(verify) if isinstance(verify, Path) else verify settings = session.merge_environment_settings( prepared_request.url, proxies, stream, verify, cert ) # Send the request. send_kwargs = { "timeout": kwargs.get("timeout", REQUESTS_TIMEOUT), "allow_redirects": kwargs.get("allow_redirects", True), } send_kwargs.update(settings) attempt = 0 resp = None while True: is_last_attempt = attempt >= 5 try: resp = session.send(prepared_request, **send_kwargs) except (requests.exceptions.ConnectionError, OSError) as e: if is_last_attempt: parsed_url = urllib.parse.urlsplit(url) exc = PoetryRuntimeError.create( reason=f"All attempts to connect to {parsed_url.netloc} failed.", exception=e, ) exc.append( ConsoleMessage( "the server is not responding to requests at the moment\n" "the hostname cannot be resolved by your DNS\n" "your network is not connected to the internet\n" ) .indent(" - ") .make_section("Probable Causes") .wrap("warning") ) exc.append( ConsoleMessage( f"Note: The path requested was {parsed_url.path}.", debug=True, ) ) raise exc else: if resp.status_code not in STATUS_FORCELIST or is_last_attempt: if raise_for_status: resp.raise_for_status() return resp if not is_last_attempt: attempt += 1 delay = self._get_backoff(resp, attempt) logger.debug("Retrying HTTP request in %s seconds.", delay) time.sleep(delay) continue # this should never really be hit under any sane circumstance raise PoetryError(f"Failed HTTP request: {method.upper()} {url}") def _get_backoff(self, response: requests.Response | None, attempt: int) -> float: if response is not None: retry_after = response.headers.get(RETRY_AFTER_HEADER, "") if retry_after: return float(retry_after) return 0.5 * attempt def get(self, url: str, **kwargs: Any) -> requests.Response: return self.request("get", url, **kwargs) def head(self, url: str, **kwargs: Any) -> requests.Response: kwargs.setdefault("allow_redirects", False) return self.request("head", url, **kwargs) def post(self, url: str, **kwargs: Any) -> requests.Response: return self.request("post", url, **kwargs) def _get_credentials_for_repository( self, repository: AuthenticatorRepositoryConfig ) -> HTTPAuthCredential: # cache repository credentials by repository url to avoid multiple keyring # backend queries when packages are being downloaded from the same source key = repository.url if key not in self._credentials: self._credentials[key] = repository.get_http_credentials( password_manager=self._password_manager ) return self._credentials[key] def _get_credentials_for_url( self, url: str, exact_match: bool = False ) -> HTTPAuthCredential: repository = self.get_repository_config_for_url(url, exact_match) credential = ( self._get_credentials_for_repository(repository=repository) if repository is not None else HTTPAuthCredential() ) if credential.password is None: parsed_url = urllib.parse.urlsplit(url) netloc = parsed_url.netloc credential = self._password_manager.get_credential( url, netloc, username=credential.username ) return HTTPAuthCredential( username=credential.username, password=credential.password ) return credential def get_credentials_for_git_url(self, url: str) -> HTTPAuthCredential: parsed_url = urllib.parse.urlsplit(url) if parsed_url.scheme not in {"http", "https"}: return HTTPAuthCredential() key = f"git+{url}" if key not in self._credentials: self._credentials[key] = self._get_credentials_for_url(url, True) return self._credentials[key] def get_credentials_for_url(self, url: str) -> HTTPAuthCredential: parsed_url = urllib.parse.urlsplit(url) netloc = parsed_url.netloc if url not in self._credentials: if "@" not in netloc: # no credentials were provided in the url, try finding the # best repository configuration self._credentials[url] = self._get_credentials_for_url(url) else: # Split from the right because that's how urllib.parse.urlsplit() # behaves if more than one @ is present (which can be checked using # the password attribute of urlsplit()'s return value). auth, netloc = netloc.rsplit("@", 1) # Split from the left because that's how urllib.parse.urlsplit() # behaves if more than one : is present (which again can be checked # using the password attribute of the return value) user, password = auth.split(":", 1) if ":" in auth else (auth, "") self._credentials[url] = HTTPAuthCredential( urllib.parse.unquote(user), urllib.parse.unquote(password), ) return self._credentials[url] def get_pypi_token(self, name: str) -> str | None: return self._password_manager.get_pypi_token(name) def get_http_auth(self, name: str) -> HTTPAuthCredential | None: if name == "pypi": repository = AuthenticatorRepositoryConfig( name, "https://upload.pypi.org/legacy/" ) else: if name not in self.configured_repositories: return None repository = self.configured_repositories[name] return self._get_credentials_for_repository(repository=repository) def get_certs_for_repository(self, name: str) -> RepositoryCertificateConfig: if name.lower() == "pypi" or name not in self.configured_repositories: return RepositoryCertificateConfig() return self.configured_repositories[name].certs(self._config) @property def configured_repositories(self) -> dict[str, AuthenticatorRepositoryConfig]: if self._configured_repositories is None: self._configured_repositories = {} for repository_name in self._config.get("repositories", []): url = self._config.get(f"repositories.{repository_name}.url") self._configured_repositories[repository_name] = ( AuthenticatorRepositoryConfig(repository_name, url) ) return self._configured_repositories def reset_credentials_cache(self) -> None: self.get_repository_config_for_url.cache_clear() self._credentials = {} def add_repository(self, name: str, url: str) -> None: self.configured_repositories[name] = AuthenticatorRepositoryConfig(name, url) self.reset_credentials_cache() def get_certs_for_url(self, url: str) -> RepositoryCertificateConfig: if url not in self._certs: self._certs[url] = self._get_certs_for_url(url) return self._certs[url] def _get_repository_config_for_url( self, url: str, exact_match: bool = False ) -> AuthenticatorRepositoryConfig | None: parsed_url = urllib.parse.urlsplit(url) candidates_netloc_only = [] candidates_path_match = [] for repository in self.configured_repositories.values(): if exact_match: if parsed_url.path == repository.path: return repository continue if repository.netloc == parsed_url.netloc: if parsed_url.path.startswith(repository.path) or commonprefix( (parsed_url.path, repository.path) ): candidates_path_match.append(repository) continue candidates_netloc_only.append(repository) if candidates_path_match: candidates = candidates_path_match elif candidates_netloc_only: candidates = candidates_netloc_only else: return None if len(candidates) > 1: logger.debug( "Multiple source configurations found for %s - %s", parsed_url.netloc, ", ".join(c.name for c in candidates), ) # prefer the more specific path candidates.sort( key=lambda c: len(commonprefix([parsed_url.path, c.path])), reverse=True ) return candidates[0] def _get_certs_for_url(self, url: str) -> RepositoryCertificateConfig: selected = self.get_repository_config_for_url(url) if selected: return selected.certs(config=self._config) return RepositoryCertificateConfig() _authenticator: Authenticator | None = None def get_default_authenticator() -> Authenticator: global _authenticator if _authenticator is None: _authenticator = Authenticator() return _authenticator ================================================ FILE: src/poetry/utils/cache.py ================================================ from __future__ import annotations import dataclasses import hashlib import json import logging import shutil import threading import time from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Generic from typing import TypeVar from typing import overload from poetry.utils._compat import decode from poetry.utils._compat import encode from poetry.utils.helpers import get_highest_priority_hash_type from poetry.utils.wheel import InvalidWheelNameError from poetry.utils.wheel import Wheel if TYPE_CHECKING: from collections.abc import Callable from poetry.core.packages.utils.link import Link from poetry.utils.env import Env # Used by FileCache for items that do not expire. MAX_DATE = 9999999999 T = TypeVar("T") logger = logging.getLogger(__name__) def _expiration(minutes: int) -> int: """ Calculates the time in seconds since epoch that occurs 'minutes' from now. :param minutes: The number of minutes to count forward """ return round(time.time()) + minutes * 60 _HASHES = { "md5": (hashlib.md5, 2), "sha1": (hashlib.sha1, 4), "sha256": (hashlib.sha256, 8), } @dataclasses.dataclass(frozen=True) class CacheItem(Generic[T]): """ Stores data and metadata for cache items. """ data: T expires: int | None = None @property def expired(self) -> bool: """ Return true if the cache item has exceeded its expiration period. """ return self.expires is not None and time.time() >= self.expires @dataclasses.dataclass(frozen=True) class FileCache(Generic[T]): """ Cachy-compatible minimal file cache. Stores subsequent data in a JSON format. :param path: The path that the cache starts at. :param hash_type: The hash to use for encoding keys/building directories. """ path: Path hash_type: str = "sha256" def __post_init__(self) -> None: if self.hash_type not in _HASHES: raise ValueError( f"FileCache.hash_type is unknown value: '{self.hash_type}'." ) def get(self, key: str) -> T | None: return self._get_payload(key) def has(self, key: str) -> bool: """ Determine if a file exists and has not expired in the cache. :param key: The cache key :returns: True if the key exists in the cache """ return self.get(key) is not None def put(self, key: str, value: Any, minutes: int | None = None) -> None: """ Store an item in the cache. :param key: The cache key :param value: The cache value :param minutes: The lifetime in minutes of the cached value """ payload: CacheItem[Any] = CacheItem( value, expires=_expiration(minutes) if minutes is not None else None ) path = self._path(key) path.parent.mkdir(parents=True, exist_ok=True) with path.open("wb") as f: f.write(self._serialize(payload)) def forget(self, key: str) -> None: """ Remove an item from the cache. :param key: The cache key """ path = self._path(key) if path.exists(): path.unlink() def flush(self) -> None: """ Clear the cache. """ shutil.rmtree(self.path) def remember( self, key: str, callback: T | Callable[[], T], minutes: int | None = None ) -> T: """ Get an item from the cache, or use a default from callback. :param key: The cache key :param callback: Callback function providing default value :param minutes: The lifetime in minutes of the cached value """ value = self.get(key) if value is None: value = callback() if callable(callback) else callback self.put(key, value, minutes) return value def _get_payload(self, key: str) -> T | None: path = self._path(key) if not path.exists(): return None with path.open("rb") as f: file_content = f.read() try: payload = self._deserialize(file_content) except (json.JSONDecodeError, ValueError): self.forget(key) logger.warning("Corrupt cache file was detected and cleaned up.") return None if payload.expired: self.forget(key) return None else: return payload.data def _path(self, key: str) -> Path: hash_type, parts_count = _HASHES[self.hash_type] h = hash_type(encode(key)).hexdigest() parts = [h[i : i + 2] for i in range(0, len(h), 2)][:parts_count] return Path(self.path, *parts, h) def _serialize(self, payload: CacheItem[T]) -> bytes: expires = payload.expires or MAX_DATE data = json.dumps(payload.data) return encode(f"{expires:010d}{data}") def _deserialize(self, data_raw: bytes) -> CacheItem[T]: data_str = decode(data_raw) data = json.loads(data_str[10:]) expires = int(data_str[:10]) return CacheItem(data, expires) class ArtifactCache: def __init__(self, *, cache_dir: Path) -> None: self._cache_dir = cache_dir self._archive_locks: defaultdict[Path, threading.Lock] = defaultdict( threading.Lock ) def get_cache_directory_for_link(self, link: Link) -> Path: key_parts = {"url": link.url_without_fragment} if hash_name := get_highest_priority_hash_type(link.hashes, link.filename): key_parts[hash_name] = link.hashes[hash_name] if link.subdirectory_fragment: key_parts["subdirectory"] = link.subdirectory_fragment return self._get_directory_from_hash(key_parts) def _get_directory_from_hash(self, key_parts: object) -> Path: key = hashlib.sha256( json.dumps( key_parts, sort_keys=True, separators=(",", ":"), ensure_ascii=True ).encode("ascii") ).hexdigest() split_key = [key[:2], key[2:4], key[4:6], key[6:]] return self._cache_dir.joinpath(*split_key) def get_cache_directory_for_git( self, url: str, ref: str, subdirectory: str | None ) -> Path: key_parts = {"url": url, "ref": ref} if subdirectory: key_parts["subdirectory"] = subdirectory return self._get_directory_from_hash(key_parts) @overload def get_cached_archive_for_link( self, link: Link, *, strict: bool, env: Env | None = ..., download_func: Callable[[str, Path], None], ) -> Path: ... @overload def get_cached_archive_for_link( self, link: Link, *, strict: bool, env: Env | None = ..., download_func: None = ..., ) -> Path | None: ... def get_cached_archive_for_link( self, link: Link, *, strict: bool, env: Env | None = None, download_func: Callable[[str, Path], None] | None = None, ) -> Path | None: cache_dir = self.get_cache_directory_for_link(link) cached_archive = self._get_cached_archive( cache_dir, strict=strict, filename=link.filename, env=env ) if cached_archive is None and strict and download_func is not None: cached_archive = cache_dir / link.filename with self._archive_locks[cached_archive]: # Check again if the archive exists (under the lock) to avoid # duplicate downloads because it may have already been downloaded # by another thread in the meantime if not cached_archive.exists(): cache_dir.mkdir(parents=True, exist_ok=True) try: download_func(link.url, cached_archive) except BaseException: cached_archive.unlink(missing_ok=True) raise return cached_archive def get_cached_archive_for_git( self, url: str, reference: str, subdirectory: str | None, env: Env ) -> Path | None: cache_dir = self.get_cache_directory_for_git(url, reference, subdirectory) return self._get_cached_archive(cache_dir, strict=False, env=env) def _get_cached_archive( self, cache_dir: Path, *, strict: bool, filename: str | None = None, env: Env | None = None, ) -> Path | None: # implication "not strict -> env must not be None" assert strict or env is not None # implication "strict -> filename must not be None" assert not strict or filename is not None archives = self._get_cached_archives(cache_dir) if not archives: return None candidates: list[tuple[float | None, Path]] = [] for archive in archives: if strict: # in strict mode return the original cached archive instead of the # prioritized archive type. if filename == archive.name: return archive continue assert env is not None if archive.suffix != ".whl": candidates.append((float("inf"), archive)) continue try: wheel = Wheel(archive.name) except InvalidWheelNameError: continue if not wheel.is_supported_by_environment(env): continue candidates.append( (wheel.get_minimum_supported_index(env.supported_tags), archive), ) if not candidates: return None return min(candidates)[1] def _get_cached_archives(self, cache_dir: Path) -> list[Path]: archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"] paths: list[Path] = [] for archive_type in archive_types: paths += cache_dir.glob(f"*.{archive_type}") return paths ================================================ FILE: src/poetry/utils/constants.py ================================================ from __future__ import annotations import os # Name of Poetry's own system project used by `poetry self` commands. POETRY_SYSTEM_PROJECT_NAME = "poetry-instance" # Timeout for HTTP requests using the requests library. REQUESTS_TIMEOUT = int(os.getenv("POETRY_REQUESTS_TIMEOUT", 15)) RETRY_AFTER_HEADER = "retry-after" # Server response codes to retry requests on. STATUS_FORCELIST = [429, 500, 501, 502, 503, 504] ================================================ FILE: src/poetry/utils/dependency_specification.py ================================================ from __future__ import annotations import contextlib import os import re import urllib.parse from pathlib import Path from typing import TYPE_CHECKING from typing import TypeVar from typing import cast from poetry.core.packages.dependency import Dependency from tomlkit.items import InlineTable from poetry.packages.direct_origin import DirectOrigin if TYPE_CHECKING: from poetry.core.packages.vcs_dependency import VCSDependency from poetry.utils.cache import ArtifactCache from poetry.utils.env import Env DependencySpec = dict[str, str | bool | dict[str, str | bool] | list[str]] BaseSpec = TypeVar("BaseSpec", DependencySpec, InlineTable) GIT_URL_SCHEMES = {"git+http", "git+https", "git+ssh"} def dependency_to_specification( dependency: Dependency, specification: BaseSpec ) -> BaseSpec: if dependency.is_vcs(): dependency = cast("VCSDependency", dependency) assert dependency.source_url is not None specification[dependency.vcs] = dependency.source_url if dependency.reference: specification["rev"] = dependency.reference elif dependency.is_file() or dependency.is_directory(): assert dependency.source_url is not None specification["path"] = dependency.source_url elif dependency.is_url(): assert dependency.source_url is not None specification["url"] = dependency.source_url elif dependency.pretty_constraint != "*" and not dependency.constraint.is_empty(): specification["version"] = dependency.pretty_constraint if not dependency.marker.is_any(): specification["markers"] = str(dependency.marker) if dependency.extras: specification["extras"] = sorted(dependency.extras) return specification class RequirementsParser: def __init__( self, *, artifact_cache: ArtifactCache, env: Env | None = None, cwd: Path | None = None, ) -> None: self._direct_origin = DirectOrigin(artifact_cache) self._env = env self._cwd = cwd or Path.cwd() def parse(self, requirement: str) -> DependencySpec: requirement = requirement.strip() specification = self._parse_pep508(requirement) if specification is not None: return specification extras = [] extras_m = re.search(r"\[([\w\d,-_ ]+)\]$", requirement) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] requirement, _ = requirement.split("[") specification = ( self._parse_url(requirement) or self._parse_path(requirement) or self._parse_simple(requirement) ) if specification: if extras: specification.setdefault("extras", extras) return specification raise ValueError(f"Invalid dependency specification: {requirement}") def _parse_pep508(self, requirement: str) -> DependencySpec | None: if " ; " not in requirement and re.search(r"@[\^~!=<>\d]", requirement): # this is of the form package@, do not attempt to parse it return None with contextlib.suppress(ValueError): dependency = Dependency.create_from_pep_508(requirement) specification: DependencySpec = {} specification = dependency_to_specification(dependency, specification) if specification: specification["name"] = dependency.name return specification return None def _parse_git_url(self, requirement: str) -> DependencySpec | None: from poetry.core.vcs.git import Git from poetry.core.vcs.git import ParsedUrl parsed = ParsedUrl.parse(requirement) url = Git.normalize_url(requirement) pair = {"name": parsed.name, "git": url.url} if parsed.rev: pair["rev"] = url.revision if parsed.subdirectory: pair["subdirectory"] = parsed.subdirectory source_root = self._env.path.joinpath("src") if self._env else None package = self._direct_origin.get_package_from_vcs( "git", url=url.url, rev=pair.get("rev"), subdirectory=parsed.subdirectory, source_root=source_root, ) pair["name"] = package.name return pair def _parse_url(self, requirement: str) -> DependencySpec | None: url_parsed = urllib.parse.urlparse(requirement) if not (url_parsed.scheme and url_parsed.netloc): return None if url_parsed.scheme in GIT_URL_SCHEMES: return self._parse_git_url(requirement) if url_parsed.scheme in ["http", "https"]: package = self._direct_origin.get_package_from_url(requirement) assert package.source_url is not None return {"name": package.name, "url": package.source_url} return None def _parse_path(self, requirement: str) -> DependencySpec | None: if (os.path.sep in requirement or "/" in requirement) and ( self._cwd.joinpath(requirement).exists() or ( Path(requirement).expanduser().exists() and Path(requirement).expanduser().is_absolute() ) ): path = Path(requirement).expanduser() is_absolute = path.is_absolute() if not path.is_absolute(): path = self._cwd.joinpath(requirement) if path.is_file(): package = self._direct_origin.get_package_from_file(path.resolve()) else: package = self._direct_origin.get_package_from_directory(path.resolve()) return { "name": package.name, "path": ( path.relative_to(self._cwd).as_posix() if not is_absolute else path.as_posix() ), } return None def _parse_simple( self, requirement: str, ) -> DependencySpec | None: extras: list[str] = [] pair = re.sub( "^([^@=: ]+)(?:@|==|(?~!])=|:| )(.*)$", "\\1 \\2", requirement ) pair = pair.strip() require: DependencySpec = {} if " " in pair: name, version = pair.split(" ", 1) extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] name, _ = name.split("[") require["name"] = name if version != "latest": require["version"] = version else: m = re.match( r"^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip() ) if m: name, constraint = m.group(1), m.group(2) extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] name, _ = name.split("[") require["name"] = name require["version"] = constraint else: extras_m = re.search(r"\[([\w\d,-_]+)\]$", pair) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] pair, _ = pair.split("[") require["name"] = pair if extras: require["extras"] = extras return require ================================================ FILE: src/poetry/utils/env/__init__.py ================================================ from __future__ import annotations from contextlib import contextmanager from pathlib import Path from tempfile import TemporaryDirectory from typing import TYPE_CHECKING from poetry.utils.env.base_env import Env from poetry.utils.env.env_manager import EnvManager from poetry.utils.env.exceptions import EnvCommandError from poetry.utils.env.exceptions import EnvError from poetry.utils.env.exceptions import IncorrectEnvError from poetry.utils.env.generic_env import GenericEnv from poetry.utils.env.mock_env import MockEnv from poetry.utils.env.null_env import NullEnv from poetry.utils.env.script_strings import GET_BASE_PREFIX from poetry.utils.env.script_strings import GET_ENV_PATH_ONELINER from poetry.utils.env.script_strings import GET_ENVIRONMENT_INFO from poetry.utils.env.script_strings import GET_PATHS from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER from poetry.utils.env.script_strings import GET_SYS_PATH from poetry.utils.env.site_packages import SitePackages from poetry.utils.env.system_env import SystemEnv from poetry.utils.env.virtual_env import VirtualEnv if TYPE_CHECKING: from collections.abc import Iterator from cleo.io.io import IO from poetry.poetry import Poetry @contextmanager def ephemeral_environment( executable: Path | None = None, flags: dict[str, str | bool] | None = None, ) -> Iterator[VirtualEnv]: with TemporaryDirectory(ignore_cleanup_errors=True) as tmp_dir: # TODO: cache PEP 517 build environment corresponding to each project venv venv_dir = Path(tmp_dir) / ".venv" EnvManager.build_venv( path=venv_dir, executable=executable, flags=flags, ) yield VirtualEnv(venv_dir, venv_dir) @contextmanager def build_environment( poetry: Poetry, env: Env | None = None, io: IO | None = None ) -> Iterator[Env]: """ If a build script is specified for the project, there could be additional build time dependencies, eg: cython, setuptools etc. In these cases, we create an ephemeral build environment with all requirements specified under `build-system.requires` and return this. Otherwise, the given default project environment is returned. """ if not env or poetry.package.build_script: with ephemeral_environment( executable=env.python if env else None, flags={"no-pip": True}, ) as venv: if io: requires = [ f"{requirement}" for requirement in poetry.pyproject.build_system.requires ] io.write_error_line( "Preparing build environment with build-system requirements" f" {', '.join(requires)}" ) from poetry.utils.isolated_build import IsolatedEnv isolated_env = IsolatedEnv(venv, poetry.pool) isolated_env.install(poetry.pyproject.build_system.requires) yield venv else: yield env __all__ = [ "GET_BASE_PREFIX", "GET_ENVIRONMENT_INFO", "GET_ENV_PATH_ONELINER", "GET_PATHS", "GET_PYTHON_VERSION_ONELINER", "GET_SYS_PATH", "Env", "EnvCommandError", "EnvError", "EnvManager", "GenericEnv", "IncorrectEnvError", "MockEnv", "NullEnv", "SitePackages", "SystemEnv", "VirtualEnv", "build_environment", "ephemeral_environment", ] ================================================ FILE: src/poetry/utils/env/base_env.py ================================================ from __future__ import annotations import contextlib import os import re import subprocess import sys import sysconfig from abc import ABC from abc import abstractmethod from functools import cached_property from pathlib import Path from subprocess import CalledProcessError from typing import TYPE_CHECKING from typing import Any from typing import TypedDict from installer.utils import SCHEME_NAMES from virtualenv.seed.wheels.embed import get_embed_wheel from poetry.utils.env.exceptions import EnvCommandError from poetry.utils.env.site_packages import SitePackages from poetry.utils.helpers import get_real_windows_path from poetry.utils.helpers import is_dir_writable if TYPE_CHECKING: from packaging.tags import Tag from poetry.core.version.markers import BaseMarker from virtualenv.seed.wheels.util import Wheel from poetry.utils.env.generic_env import GenericEnv PythonVersion = tuple[int, int, int, str, int] class MarkerEnv(TypedDict): implementation_name: str implementation_version: str os_name: str platform_machine: str platform_release: str platform_system: str platform_version: str python_full_version: str platform_python_implementation: str python_version: str sys_platform: str version_info: PythonVersion interpreter_name: str interpreter_version: str sysconfig_platform: str free_threading: bool class Env(ABC): """ An abstract Python environment. """ def __init__(self, path: Path, base: Path | None = None) -> None: self._is_windows = sys.platform == "win32" self._is_mingw = sysconfig.get_platform().startswith("mingw") self._is_conda = bool(os.environ.get("CONDA_DEFAULT_ENV")) if self._is_windows: path = get_real_windows_path(path) base = get_real_windows_path(base) if base else None bin_dir = "bin" if not self._is_windows or self._is_mingw else "Scripts" self._path = path self._bin_dir = self._path / bin_dir self._executable = "python" self._pip_executable = "pip" self.find_executables() self._base = base or path self._site_packages: SitePackages | None = None self._supported_tags: list[Tag] | None = None self._purelib: Path | None = None self._platlib: Path | None = None self._script_dirs: list[Path] | None = None self._embedded_pip_path: Path | None = None @property def bin_dir(self) -> Path: return self._bin_dir @property def path(self) -> Path: return self._path @property def base(self) -> Path: return self._base @property def version_info(self) -> PythonVersion: version_info: PythonVersion = self.marker_env["version_info"] return version_info @property def python_implementation(self) -> str: implementation: str = self.marker_env["platform_python_implementation"] return implementation @property def python(self) -> Path: """ Path to current python executable """ return Path(self._bin(self._executable)) @cached_property def marker_env(self) -> MarkerEnv: return self.get_marker_env() @property def parent_env(self) -> GenericEnv: from poetry.utils.env.generic_env import GenericEnv return GenericEnv(self.base, child_env=self) def _find_python_executable(self) -> None: bin_dir = self._bin_dir if self._is_windows and self._is_conda: bin_dir = self._path python_executables = sorted( p.name for p in bin_dir.glob("python*") if re.match(r"python(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name) ) if python_executables: executable = python_executables[0] if executable.endswith(".exe"): executable = executable[:-4] self._executable = executable def _find_pip_executable(self) -> None: pip_executables = sorted( p.name for p in self._bin_dir.glob("pip*") if re.match(r"pip(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name) ) if pip_executables: pip_executable = pip_executables[0] if pip_executable.endswith(".exe"): pip_executable = pip_executable[:-4] self._pip_executable = pip_executable def find_executables(self) -> None: self._find_python_executable() self._find_pip_executable() def get_embedded_wheel(self, distribution: str) -> Path: wheel: Wheel = get_embed_wheel( distribution, f"{self.version_info[0]}.{self.version_info[1]}" ) path: Path = wheel.path return path @property def pip_embedded(self) -> Path: if self._embedded_pip_path is None: self._embedded_pip_path = self.get_embedded_wheel("pip") / "pip" return self._embedded_pip_path @property def pip(self) -> Path: """ Path to current pip executable """ # we do not use as_posix() here due to issues with windows pathlib2 # implementation path = Path(self._bin(self._pip_executable)) if not path.exists(): return self.pip_embedded return path @property def platform(self) -> str: return sys.platform @property def os(self) -> str: return os.name @property def site_packages(self) -> SitePackages: if self._site_packages is None: self._site_packages = SitePackages( self.purelib, self.platlib, self.fallbacks, ) return self._site_packages @property def usersite(self) -> Path | None: if "usersite" in self.paths: return Path(self.paths["usersite"]) return None @property def userbase(self) -> Path | None: if "userbase" in self.paths: return Path(self.paths["userbase"]) return None @property def purelib(self) -> Path: if self._purelib is None: self._purelib = Path(self.paths["purelib"]) return self._purelib @property def platlib(self) -> Path: if self._platlib is None: if "platlib" in self.paths: self._platlib = Path(self.paths["platlib"]) else: self._platlib = self.purelib return self._platlib @cached_property def fallbacks(self) -> list[Path]: paths = [Path(path) for path in self.paths.get("fallbacks", [])] paths += [self.usersite] if self.usersite else [] return paths def set_paths( self, purelib: str | Path | None = None, platlib: str | Path | None = None, userbase: str | Path | None = None, usersite: str | Path | None = None, ) -> None: """ A cached property aware way to set environment paths during runtime. In some cases, like in `PluginManager._install()` method, paths are modified during execution. Direct modification of `self.paths` is not safe as caches relying on are not invalidated. This helper method ensures that we clear the relevant caches why paths are modified. """ if purelib: self.paths["purelib"] = str(purelib) if platlib: self.paths["platlib"] = str(platlib) if userbase: self.paths["userbase"] = str(userbase) if usersite: self.paths["usersite"] = str(usersite) # clear cached properties using the env paths self.__dict__.pop("fallbacks", None) self.__dict__.pop("scheme_dict", None) @cached_property def scheme_dict(self) -> dict[str, str]: """ This property exists to allow cases where system environment paths are not writable and user site is enabled. This enables us to ensure packages (wheels) are correctly installed into directories where the current user can write to. If all candidates in `self.paths` is writable, no modification is made. If at least one path is not writable and all generated writable candidates are indeed writable, these are used instead. If any candidate is not writable, the original paths are returned. Alternative writable candidates are generated by replacing discovered prefix, with "userbase" if available. The original prefix is computed as the common path prefix of "scripts" and "purelib". For example, given `{ "purelib": "/usr/local/lib/python3.13/site-packages", "scripts": "/usr/local/bin", "userbase": "/home/user/.local" }`; the candidate "purelib" path would be `/home/user/.local/lib/python3.13/site-packages`. """ paths = self.paths.copy() if ( not self.is_venv() and paths.get("userbase") and ("scripts" in paths and "purelib" in paths) ): overrides: dict[str, str] = {} try: base_path = os.path.commonpath([paths["scripts"], paths["purelib"]]) except ValueError: return paths scheme_names = [key for key in SCHEME_NAMES if key in self.paths] for key in scheme_names: if not is_dir_writable(path=Path(paths[key]), create=True): # there is at least one path that is not writable break else: # all paths are writable, return early return paths for key in scheme_names: candidate = paths[key].replace(base_path, paths["userbase"]) if not is_dir_writable(path=Path(candidate), create=True): # at least one candidate is not writable, we cannot do much here return paths overrides[key] = candidate paths.update(overrides) return paths def _get_lib_dirs(self) -> list[Path]: return [self.purelib, self.platlib, *self.fallbacks] def is_path_relative_to_lib(self, path: Path) -> bool: for lib_path in self._get_lib_dirs(): with contextlib.suppress(ValueError): path.relative_to(lib_path) return True return False @property @abstractmethod def sys_path(self) -> list[str]: ... @cached_property def paths(self) -> dict[str, str]: paths = self.get_paths() if self.is_venv(): # We copy pip's logic here for the `include` path paths["include"] = str( self.path.joinpath( "include", "site", f"python{self.version_info[0]}.{self.version_info[1]}", ) ) return paths @property def supported_tags(self) -> list[Tag]: if self._supported_tags is None: self._supported_tags = self.get_supported_tags() return self._supported_tags @classmethod def get_base_prefix(cls) -> Path: real_prefix = getattr(sys, "real_prefix", None) if real_prefix is not None: return Path(real_prefix) base_prefix = getattr(sys, "base_prefix", None) if base_prefix is not None: return Path(base_prefix) return Path(sys.prefix) @abstractmethod def get_marker_env(self) -> MarkerEnv: ... def get_pip_command(self, embedded: bool = False) -> list[str]: if embedded or not Path(self._bin(self._pip_executable)).exists(): return [str(self.python), str(self.pip_embedded)] # run as module so that pip can update itself on Windows return [str(self.python), "-m", "pip"] @abstractmethod def get_supported_tags(self) -> list[Tag]: ... @abstractmethod def get_paths(self) -> dict[str, str]: ... def is_valid_for_marker(self, marker: BaseMarker) -> bool: valid: bool = marker.validate(self.marker_env) return valid def is_sane(self) -> bool: """ Checks whether the current environment is sane or not. """ return True def get_command_from_bin(self, bin: str) -> list[str]: if bin == "pip": # when pip is required we need to ensure that we fall back to # embedded pip when pip is not available in the environment return self.get_pip_command() return [self._bin(bin)] def run(self, bin: str, *args: str, **kwargs: Any) -> str: cmd = self.get_command_from_bin(bin) + list(args) return self._run(cmd, **kwargs) def run_pip(self, *args: str, **kwargs: Any) -> str: pip = self.get_pip_command() cmd = pip + list(args) return self._run(cmd, **kwargs) def run_python_script(self, content: str, **kwargs: Any) -> str: # Options Used: # -I : Run Python in isolated mode. (#6627) # -W ignore : Suppress warnings. # # TODO: Consider replacing (-I) with (-EP) once support for managing Python <3.11 environments dropped. # This is useful to prevent user site being disabled over zealously. return self.run( self._executable, "-I", "-W", "ignore", "-c", content, stderr=subprocess.PIPE, **kwargs, ) def _run(self, cmd: list[str], **kwargs: Any) -> str: """ Run a command inside the Python environment. """ call = kwargs.pop("call", False) env = kwargs.pop("env", dict(os.environ)) stderr = kwargs.pop("stderr", subprocess.STDOUT) try: if call: assert stderr != subprocess.PIPE subprocess.check_call(cmd, stderr=stderr, env=env, **kwargs) output = "" else: output = subprocess.check_output( cmd, stderr=stderr, env=env, text=True, encoding="locale", **kwargs ) except CalledProcessError as e: raise EnvCommandError(e) return output def execute(self, bin: str, *args: str, **kwargs: Any) -> int: command = self.get_command_from_bin(bin) + list(args) env = kwargs.pop("env", dict(os.environ)) if not self._is_windows: return os.execvpe(command[0], command, env=env) kwargs["shell"] = True exe = subprocess.Popen(command, env=env, **kwargs) exe.communicate() return exe.returncode @abstractmethod def is_venv(self) -> bool: ... @property def script_dirs(self) -> list[Path]: if self._script_dirs is None: scripts = self.paths.get("scripts") self._script_dirs = [ Path(scripts) if scripts is not None else self._bin_dir ] if self.userbase: self._script_dirs.append(self.userbase / self._script_dirs[0].name) return self._script_dirs def _bin(self, bin: str) -> str: """ Return path to the given executable. """ if self._is_windows and not bin.endswith(".exe"): bin_path = self._bin_dir / (bin + ".exe") else: bin_path = self._bin_dir / bin if not bin_path.exists(): # On Windows, some executables can be in the base path # This is especially true when installing Python with # the official installer, where python.exe will be at # the root of the env path. if self._is_windows: if not bin.endswith(".exe"): bin_path = self._path / (bin + ".exe") else: bin_path = self._path / bin if bin_path.exists(): return str(bin_path) return bin return str(bin_path) def __eq__(self, other: object) -> bool: if not isinstance(other, Env): return False return other.__class__ == self.__class__ and other.path == self.path def __repr__(self) -> str: return f'{self.__class__.__name__}("{self._path}")' ================================================ FILE: src/poetry/utils/env/env_manager.py ================================================ from __future__ import annotations import base64 import hashlib import os import plistlib import re import subprocess import sys from functools import cached_property from pathlib import Path from subprocess import CalledProcessError from typing import TYPE_CHECKING import tomlkit import virtualenv from cleo.io.null_io import NullIO from poetry.core.constraints.version import Version from poetry.console.exceptions import PoetryConsoleError from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS from poetry.utils._compat import encode from poetry.utils.env.exceptions import EnvCommandError from poetry.utils.env.exceptions import IncorrectEnvError from poetry.utils.env.generic_env import GenericEnv from poetry.utils.env.python import Python from poetry.utils.env.python.exceptions import InvalidCurrentPythonVersionError from poetry.utils.env.python.exceptions import NoCompatiblePythonVersionFoundError from poetry.utils.env.python.exceptions import PythonVersionNotFoundError from poetry.utils.env.script_strings import GET_ENV_PATH_ONELINER from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER from poetry.utils.env.system_env import SystemEnv from poetry.utils.env.virtual_env import VirtualEnv from poetry.utils.helpers import get_real_windows_path from poetry.utils.helpers import remove_directory if TYPE_CHECKING: from cleo.io.io import IO from poetry.poetry import Poetry from poetry.utils.env.base_env import Env class EnvsFile(TOMLFile): """ This file contains one section per project with the project's base env name as section name. Each section contains the minor and patch version of the python executable used to create the currently active virtualenv. Example: [poetry-QRErDmmj] minor = "3.9" patch = "3.9.13" [poetry-core-m5r7DkRA] minor = "3.11" patch = "3.11.6" """ def remove_section(self, name: str, minor: str | None = None) -> str | None: """ Remove a section from the envs file. If "minor" is given, the section is only removed if its minor value matches "minor". Returns the "minor" value of the removed section. """ envs = self.read() current_env = envs.get(name) if current_env is not None and (not minor or current_env["minor"] == minor): del envs[name] self.write(envs) minor = current_env["minor"] assert isinstance(minor, str) return minor return None class EnvManager: """ Environments manager """ _env = None ENVS_FILE = "envs.toml" def __init__(self, poetry: Poetry, io: None | IO = None) -> None: self._poetry = poetry self._io = io or NullIO() @property def in_project_venv(self) -> Path: venv: Path = self._poetry.file.path.parent / ".venv" return venv @cached_property def envs_file(self) -> EnvsFile: return EnvsFile(self._poetry.config.virtualenvs_path / self.ENVS_FILE) @cached_property def base_env_name(self) -> str: return self.generate_env_name( self._poetry.package.name, str(self._poetry.file.path.parent), ) def activate(self, python: str) -> Env: venv_path = self._poetry.config.virtualenvs_path python_instance = Python.get_by_name(python) if python_instance is None: raise PythonVersionNotFoundError(python) create = False # If we are required to create the virtual environment in the project directory, # create or recreate it if needed if self.use_in_project_venv(): create = False venv = self.in_project_venv if venv.exists(): # We need to check if the patch version is correct _venv = VirtualEnv(venv) current_patch = ".".join(str(v) for v in _venv.version_info[:3]) if python_instance.patch_version.to_string() != current_patch: create = True self.create_venv(python=python_instance, force=create) return self.get(reload=True) envs = tomlkit.document() if self.envs_file.exists(): envs = self.envs_file.read() current_env = envs.get(self.base_env_name) if current_env is not None: current_minor = current_env["minor"] current_patch = current_env["patch"] if ( current_minor == python_instance.minor_version.to_string() and current_patch != python_instance.patch_version.to_string() ): # We need to recreate create = True venv = ( venv_path / f"{self.base_env_name}-py{python_instance.minor_version.to_string()}" ) # Create if needed if not venv.exists() or create: in_venv = os.environ.get("VIRTUAL_ENV") is not None if in_venv or not venv.exists(): create = True if venv.exists(): # We need to check if the patch version is correct _venv = VirtualEnv(venv) current_patch = ".".join(str(v) for v in _venv.version_info[:3]) if python_instance.patch_version.to_string() != current_patch: create = True self.create_venv(python=python_instance, force=create) # Activate envs[self.base_env_name] = { "minor": python_instance.minor_version.to_string(), "patch": python_instance.patch_version.to_string(), } self.envs_file.write(envs) return self.get(reload=True) def deactivate(self) -> None: venv_path = self._poetry.config.virtualenvs_path if self.envs_file.exists() and ( minor := self.envs_file.remove_section(self.base_env_name) ): venv = venv_path / f"{self.base_env_name}-py{minor}" self._io.write_error_line( f"Deactivating virtualenv: {venv}" ) def get(self, reload: bool = False) -> Env: if self._env is not None and not reload: return self._env python_minor: str | None = None env = None envs = None if self.envs_file.exists(): envs = self.envs_file.read() env = envs.get(self.base_env_name) if env: python_minor = env["minor"] # Check if we are inside a virtualenv or not # Conda sets CONDA_PREFIX in its envs, see # https://github.com/conda/conda/issues/2764 env_prefix = os.environ.get("VIRTUAL_ENV", os.environ.get("CONDA_PREFIX")) conda_env_name = os.environ.get("CONDA_DEFAULT_ENV") # It's probably not a good idea to pollute Conda's global "base" env, since # most users have it activated all the time. in_venv = env_prefix is not None and conda_env_name != "base" if not in_venv or env is not None: # Checking if a local virtualenv exists if self.in_project_venv_exists(): venv = self.in_project_venv return VirtualEnv(venv) create_venv = self._poetry.config.get("virtualenvs.create", True) if not create_venv: return self.get_system_env() venv_path = self._poetry.config.virtualenvs_path if python_minor is None: # we only need to discover python version in this case python = Python.get_preferred_python( config=self._poetry.config, io=self._io ) python_minor = python.minor_version.to_string() name = f"{self.base_env_name}-py{python_minor.strip()}" venv = venv_path / name if not venv.exists(): if env and envs: del envs[self.base_env_name] self.envs_file.write(envs) return self.get_system_env() return VirtualEnv(venv) if env_prefix is not None: prefix = Path(env_prefix) base_prefix = None else: prefix = Path(sys.prefix) base_prefix = self.get_base_prefix() return VirtualEnv(prefix, base_prefix) def list(self, name: str | None = None) -> list[VirtualEnv]: if name is None: name = self._poetry.package.name venv_name = self.generate_env_name(name, str(self._poetry.file.path.parent)) venv_path = self._poetry.config.virtualenvs_path env_list = [VirtualEnv(p) for p in sorted(venv_path.glob(f"{venv_name}-py*"))] if self.in_project_venv_exists(): venv = self.in_project_venv env_list.insert(0, VirtualEnv(venv)) return env_list @staticmethod def check_env_is_for_current_project(env: str, base_env_name: str) -> bool: """ Check if env name starts with projects name. This is done to prevent action on other project's envs. """ return env.startswith(base_env_name) def remove(self, python: str) -> Env: python_path = Path(python) if python_path.is_file(): # Validate env name if provided env is a full path to python try: env_dir = subprocess.check_output( [python, "-c", GET_ENV_PATH_ONELINER], text=True, encoding="locale" ).strip("\n") env_name = Path(env_dir).name if not self.check_env_is_for_current_project( env_name, self.base_env_name ): raise IncorrectEnvError(env_name) except CalledProcessError as e: raise EnvCommandError(e) if self.check_env_is_for_current_project(python, self.base_env_name): venvs = self.list() for venv in venvs: if venv.path.name == python: # Exact virtualenv name if self.envs_file.exists(): venv_minor = ".".join(str(v) for v in venv.version_info[:2]) self.envs_file.remove_section(self.base_env_name, venv_minor) self.remove_venv(venv.path) return venv raise ValueError( f'Environment "{python}" does not exist.' ) else: venv_path = self._poetry.config.virtualenvs_path # Get all the poetry envs, even for other projects env_names = [p.name for p in sorted(venv_path.glob("*-*-py*"))] if python in env_names: raise IncorrectEnvError(python) try: python_version = Version.parse(python) python = f"python{python_version.major}" if python_version.precision > 1: python += f".{python_version.minor}" except ValueError: # Executable in PATH or full executable path pass try: python_version_string = subprocess.check_output( [python, "-c", GET_PYTHON_VERSION_ONELINER], text=True, encoding="locale", ) except CalledProcessError as e: raise EnvCommandError(e) python_version = Version.parse(python_version_string.strip()) minor = f"{python_version.major}.{python_version.minor}" name = f"{self.base_env_name}-py{minor}" venv_path = venv_path / name if not venv_path.exists(): raise ValueError(f'Environment "{name}" does not exist.') if self.envs_file.exists(): self.envs_file.remove_section(self.base_env_name, minor) self.remove_venv(venv_path) return VirtualEnv(venv_path, venv_path) def use_in_project_venv(self) -> bool: in_project: bool | None = self._poetry.config.get("virtualenvs.in-project") if in_project is not None: return in_project return self.in_project_venv.is_dir() def in_project_venv_exists(self) -> bool: in_project: bool | None = self._poetry.config.get("virtualenvs.in-project") if in_project is False: return False return self.in_project_venv.is_dir() def create_venv( self, name: str | None = None, python: Python | None = None, force: bool = False, ) -> Env: if self._env is not None and not force: return self._env cwd = self._poetry.file.path.parent env = self.get(reload=True) if not env.is_sane(): force = True if env.is_venv() and not force: # Already inside a virtualenv. current_python = Version.parse( ".".join(str(c) for c in env.version_info[:3]) ) if not self._poetry.package.python_constraint.allows(current_python): raise InvalidCurrentPythonVersionError( self._poetry.package.python_versions, str(current_python) ) return env create_venv = self._poetry.config.get("virtualenvs.create") in_project_venv = self.use_in_project_venv() venv_prompt = self._poetry.config.get("virtualenvs.prompt") specific_python_requested = python is not None if not python: python = Python.get_preferred_python( config=self._poetry.config, io=self._io ) venv_path = ( self.in_project_venv if in_project_venv else self._poetry.config.virtualenvs_path ) if not name: name = self._poetry.package.name supported_python = self._poetry.package.python_constraint if not supported_python.allows(python.patch_version): # The currently activated or chosen Python version # is not compatible with the Python constraint specified # for the project. # If an executable has been specified, we stop there # and notify the user of the incompatibility. # Otherwise, we try to find a compatible Python version. if specific_python_requested: raise NoCompatiblePythonVersionFoundError( self._poetry.package.python_versions, python.patch_version.to_string(), ) self._io.write_error_line( f"The currently activated Python version {python.patch_version.to_string()} is not" f" supported by the project ({self._poetry.package.python_versions}).\n" "Trying to find and use a compatible version. " ) python = Python.get_compatible_python(poetry=self._poetry, io=self._io) if in_project_venv: venv = venv_path else: name = self.generate_env_name(name, str(cwd)) name = f"{name}-py{python.minor_version.to_string()}" venv = venv_path / name if venv_prompt is not None: try: venv_prompt = venv_prompt.format( project_name=self._poetry.package.name or "virtualenv", python_version=python.minor_version.to_string(), ) except KeyError as e: raise PoetryConsoleError( f"Invalid template variable '{e.args[0]}' in 'virtualenvs.prompt' setting.\n" f"Valid variables are: {{project_name}}, {{python_version}}" ) from e except ValueError as e: raise PoetryConsoleError( f"Invalid template string in 'virtualenvs.prompt' setting: {e}" ) from e if not venv.exists(): if create_venv is False: self._io.write_error_line( "" "Skipping virtualenv creation, " "as specified in config file." "" ) return self.get_system_env() self._io.write_error_line( f"Creating virtualenv {name} in" f" {venv_path if not WINDOWS else get_real_windows_path(venv_path)!s}" ) else: create_venv = False if force: if not env.is_sane(): self._io.write_error_line( f"The virtual environment found in {env.path} seems to" " be broken." ) self._io.write_error_line( f"Recreating virtualenv {name} in {venv!s}" ) self.remove_venv(venv) create_venv = True elif self._io.is_very_verbose(): self._io.write_error_line(f"Virtualenv {name} already exists.") if create_venv: self.build_venv( venv, executable=python.executable, flags=self._poetry.config.get("virtualenvs.options"), prompt=venv_prompt, ) # venv detection: # stdlib venv may symlink sys.executable, so we can't use realpath. # but others can symlink *to* the venv Python, # so we can't just use sys.executable. # So we just check every item in the symlink tree (generally <= 3) p = os.path.normcase(sys.executable) paths = [p] while os.path.islink(p): p = os.path.normcase(os.path.join(os.path.dirname(p), os.readlink(p))) paths.append(p) p_venv = os.path.normcase(str(venv)) if any(p.startswith(p_venv) for p in paths): # Running properly in the virtualenv, don't need to do anything return self.get_system_env() return VirtualEnv(venv) @classmethod def build_venv( cls, path: Path, executable: Path | None = None, flags: dict[str, str | bool] | None = None, with_pip: bool | None = None, prompt: str | None = None, ) -> virtualenv.run.session.Session: flags = flags or {} if with_pip is not None: flags["no-pip"] = not with_pip flags.setdefault("no-pip", True) flags.setdefault("no-setuptools", True) flags.setdefault("no-wheel", True) if WINDOWS: path = get_real_windows_path(path) executable = get_real_windows_path(executable) if executable else None executable_str = None if executable is None else executable.resolve().as_posix() args = [ "--no-download", "--no-periodic-update", "--python", executable_str or sys.executable, ] if prompt is not None: args.extend(["--prompt", prompt]) for flag, value in flags.items(): if value is True: args.append(f"--{flag}") elif value is not False: args.append(f"--{flag}={value}") args.append(str(path)) cli_result = virtualenv.cli_run(args, setup_logging=False) # Exclude the venv folder from from macOS Time Machine backups # TODO: Add backup-ignore markers for other platforms too if sys.platform == "darwin": import xattr xattr.setxattr( str(path), "com.apple.metadata:com_apple_backup_excludeItem", plistlib.dumps("com.apple.backupd", fmt=plistlib.FMT_BINARY), ) return cli_result @classmethod def remove_venv(cls, path: Path) -> None: assert path.is_dir() try: remove_directory(path) return except OSError as e: # Continue only if e.errno == 16 if e.errno != 16: # ERRNO 16: Device or resource busy raise e # Delete all files and folders but the toplevel one. This is because sometimes # the venv folder is mounted by the OS, such as in a docker volume. In such # cases, an attempt to delete the folder itself will result in an `OSError`. # See https://github.com/python-poetry/poetry/pull/2064 for file_path in path.iterdir(): if file_path.is_file() or file_path.is_symlink(): file_path.unlink() elif file_path.is_dir(): remove_directory(file_path, force=True) @classmethod def get_system_env(cls, naive: bool = False) -> Env: """ Retrieve the current Python environment. This can be the base Python environment or an activated virtual environment. This method also workaround the issue that the virtual environment used by Poetry internally (when installed via the custom installer) is incorrectly detected as the system environment. Note that this workaround happens only when `naive` is False since there are times where we actually want to retrieve Poetry's custom virtual environment (e.g. plugin installation or self update). """ prefix, base_prefix = Path(sys.prefix), Path(cls.get_base_prefix()) env: Env = SystemEnv(prefix) if not naive: env = GenericEnv(base_prefix, child_env=env) return env @classmethod def get_base_prefix(cls) -> Path: real_prefix = getattr(sys, "real_prefix", None) if real_prefix is not None: return Path(real_prefix) base_prefix = getattr(sys, "base_prefix", None) if base_prefix is not None: return Path(base_prefix) return Path(sys.prefix) @classmethod def generate_env_name(cls, name: str, cwd: str) -> str: name = name.lower() sanitized_name = re.sub(r'[ $`!*@"\\\r\n\t]', "_", name)[:42] normalized_cwd = os.path.normcase(os.path.realpath(cwd)) h_bytes = hashlib.sha256(encode(normalized_cwd)).digest() h_str = base64.urlsafe_b64encode(h_bytes).decode()[:8] return f"{sanitized_name}-{h_str}" ================================================ FILE: src/poetry/utils/env/exceptions.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.utils._compat import decode if TYPE_CHECKING: from subprocess import CalledProcessError class EnvError(Exception): pass class IncorrectEnvError(EnvError): def __init__(self, env_name: str) -> None: message = f"Env {env_name} doesn't belong to this project." super().__init__(message) class EnvCommandError(EnvError): def __init__(self, e: CalledProcessError) -> None: self.e = e message_parts = [ f"Command {e.cmd} errored with the following return code {e.returncode}" ] if e.output: message_parts.append(f"Output:\n{decode(e.output)}") if e.stderr: message_parts.append(f"Error output:\n{decode(e.stderr)}") super().__init__("\n\n".join(message_parts)) ================================================ FILE: src/poetry/utils/env/generic_env.py ================================================ from __future__ import annotations import json import os import re import subprocess from typing import TYPE_CHECKING from typing import Any from poetry.utils._compat import WINDOWS from poetry.utils.env.script_strings import GET_PATHS from poetry.utils.env.virtual_env import VirtualEnv if TYPE_CHECKING: from pathlib import Path from poetry.utils.env.base_env import Env class GenericEnv(VirtualEnv): def __init__( self, path: Path, base: Path | None = None, child_env: Env | None = None ) -> None: self._child_env = child_env super().__init__(path, base=base) def find_executables(self) -> None: patterns = [("python*", "pip*")] if self._child_env: minor_version = ( f"{self._child_env.version_info[0]}.{self._child_env.version_info[1]}" ) major_version = f"{self._child_env.version_info[0]}" patterns = [ (f"python{minor_version}", f"pip{minor_version}"), (f"python{major_version}", f"pip{major_version}"), ] if WINDOWS: patterns = [(f"{p[0]}.exe", f"{p[1]}.exe") for p in patterns] python_executable = None pip_executable = None for python_pattern, pip_pattern in patterns: if python_executable and pip_executable: break if not python_executable: python_executables = sorted( p.name for p in self._bin_dir.glob(python_pattern) if re.match(r"python(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name) ) if python_executables: executable = python_executables[0] if executable.endswith(".exe"): executable = executable[:-4] python_executable = executable if not pip_executable: pip_executables = sorted( p.name for p in self._bin_dir.glob(pip_pattern) if re.match(r"pip(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name) ) if pip_executables: pip_executable = pip_executables[0] if pip_executable.endswith(".exe"): pip_executable = pip_executable[:-4] if python_executable: self._executable = python_executable if pip_executable: self._pip_executable = pip_executable def get_paths(self) -> dict[str, str]: output = self.run_python_script(GET_PATHS) paths: dict[str, str] = json.loads(output) return paths def execute(self, bin: str, *args: str, **kwargs: Any) -> int: command = self.get_command_from_bin(bin) + list(args) env = kwargs.pop("env", dict(os.environ)) if not self._is_windows: return os.execvpe(command[0], command, env=env) exe = subprocess.Popen(command, env=env, **kwargs) exe.communicate() return exe.returncode def _run(self, cmd: list[str], **kwargs: Any) -> str: return super(VirtualEnv, self)._run(cmd, **kwargs) def is_venv(self) -> bool: return self._path != self._base ================================================ FILE: src/poetry/utils/env/mock_env.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from poetry.utils.env.null_env import NullEnv if TYPE_CHECKING: from packaging.tags import Tag from poetry.utils.env.base_env import MarkerEnv from poetry.utils.env.base_env import PythonVersion class MockEnv(NullEnv): def __init__( self, version_info: tuple[int, int, int] | PythonVersion = (3, 7, 0), *, python_implementation: str = "CPython", platform: str = "darwin", platform_machine: str = "amd64", os_name: str = "posix", is_venv: bool = False, sys_path: list[str] | None = None, marker_env: dict[str, Any] | None = None, supported_tags: list[Tag] | None = None, **kwargs: Any, ) -> None: super().__init__(**kwargs) if len(version_info) == 3: version_info = (*version_info, "final", 0) self._version_info = version_info self._python_implementation = python_implementation self._platform = platform self._platform_machine = platform_machine self._os_name = os_name self._is_venv = is_venv self._sys_path = sys_path self._mock_marker_env = marker_env self._supported_tags = supported_tags @property def platform(self) -> str: return self._platform @property def platform_machine(self) -> str: return self._platform_machine @property def os(self) -> str: return self._os_name @property def sys_path(self) -> list[str]: if self._sys_path is None: return super().sys_path return self._sys_path def get_marker_env(self) -> MarkerEnv: marker_env = super().get_marker_env() marker_env["version_info"] = self._version_info marker_env["python_version"] = ".".join(str(v) for v in self._version_info[:2]) marker_env["python_full_version"] = ".".join( str(v) for v in self._version_info[:3] ) marker_env["sys_platform"] = self._platform marker_env["platform_machine"] = self._platform_machine marker_env["interpreter_name"] = self._python_implementation.lower() marker_env["interpreter_version"] = "cp" + "".join( str(v) for v in self._version_info[:2] ) if self._mock_marker_env is not None: for key, value in self._mock_marker_env.items(): marker_env[key] = value # type: ignore[literal-required] return marker_env def is_venv(self) -> bool: return self._is_venv ================================================ FILE: src/poetry/utils/env/null_env.py ================================================ from __future__ import annotations import sys from functools import cached_property from pathlib import Path from typing import Any from poetry.utils.env.system_env import SystemEnv class NullEnv(SystemEnv): def __init__( self, path: Path | None = None, base: Path | None = None, execute: bool = False ) -> None: if path is None: path = Path(sys.prefix) super().__init__(path, base=base) self._execute = execute self.executed: list[list[str]] = [] @cached_property def paths(self) -> dict[str, str]: paths = self.get_paths() paths["platlib"] = str(self._path / "platlib") paths["purelib"] = str(self._path / "purelib") paths["scripts"] = str(self._path / "scripts") paths["data"] = str(self._path / "data") return paths def _run(self, cmd: list[str], **kwargs: Any) -> str: self.executed.append(cmd) if self._execute: return super()._run(cmd, **kwargs) return "" def execute(self, bin: str, *args: str, **kwargs: Any) -> int: self.executed.append([bin, *list(args)]) if self._execute: return super().execute(bin, *args, **kwargs) return 0 def _bin(self, bin: str) -> str: return bin ================================================ FILE: src/poetry/utils/env/python/__init__.py ================================================ from __future__ import annotations from poetry.utils.env.python.manager import Python __all__ = ["Python"] ================================================ FILE: src/poetry/utils/env/python/exceptions.py ================================================ from __future__ import annotations class PythonVersionError(Exception): pass class PythonVersionNotFoundError(PythonVersionError): def __init__(self, expected: str) -> None: super().__init__(f"Could not find the python executable {expected}") class NoCompatiblePythonVersionFoundError(PythonVersionError): def __init__(self, expected: str, given: str | None = None) -> None: if given: message = ( f"The specified Python version ({given}) " f"is not supported by the project ({expected}).\n" "Please choose a compatible version " "or loosen the python constraint specified " "in the pyproject.toml file." ) else: message = ( "Poetry was unable to find a compatible version. " "If you have one, you can explicitly use it " 'via the "env use" command.' ) super().__init__(message) class InvalidCurrentPythonVersionError(PythonVersionError): def __init__(self, expected: str, given: str) -> None: message = ( f"Current Python version ({given}) " f"is not allowed by the project ({expected}).\n" 'Please change python executable via the "env use" command.' ) super().__init__(message) ================================================ FILE: src/poetry/utils/env/python/installer.py ================================================ from __future__ import annotations import dataclasses from subprocess import CalledProcessError from typing import TYPE_CHECKING from typing import Literal import pbs_installer as pbi from poetry.core.constraints.version import Version from poetry.config.config import Config from poetry.console.exceptions import ConsoleMessage from poetry.console.exceptions import PoetryRuntimeError from poetry.utils.env.python import Python if TYPE_CHECKING: from pathlib import Path BAD_PYTHON_INSTALL_INFO = [ "This could happen because you are missing platform dependencies required.", "Please refer to https://gregoryszorc.com/docs/python-build-standalone/main/running.html#runtime-requirements " "for more information about the necessary requirements.", "Please remove the failing Python installation using poetry python remove before continuing.", ] class PythonInstallerError(Exception): pass class PythonDownloadNotFoundError(PythonInstallerError, ValueError): pass class PythonInstallationError(PythonInstallerError, ValueError): pass @dataclasses.dataclass(frozen=True) class PythonInstaller: request: str implementation: Literal["cpython", "pypy"] = dataclasses.field(default="cpython") free_threaded: bool = dataclasses.field(default=False) installation_directory: Path = dataclasses.field( init=False, default_factory=lambda: Config.create().python_installation_dir ) @property def version(self) -> Version: try: pyver, _ = pbi.get_download_link( self.request, implementation=self.implementation, free_threaded=self.free_threaded, ) return Version.from_parts( major=pyver.major, minor=pyver.minor, patch=pyver.micro ) except ValueError: raise PythonDownloadNotFoundError( "No suitable standalone build found for the requested Python version." ) def exists(self) -> bool: version = self.version bad_executables = set() for python in Python.find_poetry_managed_pythons(): try: if python.implementation.lower() != self.implementation: continue if python.free_threaded != self.free_threaded: continue if version == python.version: return True except CalledProcessError: bad_executables.add(python.executable) if bad_executables: raise PoetryRuntimeError( reason="One or more installed version do not work on your system. This is not a Poetry issue.", messages=[ ConsoleMessage("\n".join(e.as_posix() for e in bad_executables)) .indent(" - ") .make_section("Failing Executables") .wrap("info"), *[ ConsoleMessage(m).wrap("warning") for m in BAD_PYTHON_INSTALL_INFO ], ], ) return False def install(self) -> None: try: # this can be broken into download, and install_file if required to make # use of Poetry's own mechanics for download and unpack pbi.install( self.request, self.installation_directory, version_dir=True, implementation=self.implementation, free_threaded=self.free_threaded, ) except ValueError: raise PythonInstallationError( "Failed to download and install requested version of Python." ) ================================================ FILE: src/poetry/utils/env/python/manager.py ================================================ from __future__ import annotations import contextlib import os import sys from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING from typing import NamedTuple from typing import cast from typing import overload import findpython import packaging.version from cleo.io.null_io import NullIO from cleo.io.outputs.output import Verbosity from pbs_installer._install import THIS_ARCH from pbs_installer._install import THIS_PLATFORM from pbs_installer._versions import PYTHON_VERSIONS from poetry.core.constraints.version import Version from poetry.core.constraints.version import VersionConstraint from poetry.core.constraints.version import parse_constraint from poetry.utils.env.python.exceptions import NoCompatiblePythonVersionFoundError from poetry.utils.env.python.providers import PoetryPythonPathProvider from poetry.utils.env.python.providers import ShutilWhichPythonProvider if TYPE_CHECKING: from collections.abc import Iterator from cleo.io.io import IO from poetry.config.config import Config from poetry.poetry import Poetry # register default providers findpython.register_provider(PoetryPythonPathProvider) class PythonInfo(NamedTuple): major: int minor: int patch: int implementation: str free_threaded: bool executable: Path | None class Python: @overload def __init__(self, *, python: findpython.PythonVersion) -> None: ... @overload def __init__( self, executable: str | Path, version: Version | None = None ) -> None: ... # we overload __init__ to ensure we do not break any downstream plugins # that use the this def __init__( self, executable: str | Path | None = None, version: Version | None = None, python: findpython.PythonVersion | None = None, ) -> None: if python and (executable or version): raise ValueError( "When python is provided, neither executable or version must be specified" ) if python: self._python = python elif executable: self._python = findpython.PythonVersion( executable=Path(executable), _version=packaging.version.Version(str(version)) if version else None, ) else: raise ValueError("Either python or executable must be provided") @classmethod def find_all(cls) -> Iterator[Python]: venv_path: Path | None = ( Path(os.environ["VIRTUAL_ENV"]) if "VIRTUAL_ENV" in os.environ else None ) for python in findpython.find_all(): if venv_path and python.executable.is_relative_to(venv_path): continue yield cls(python=python) @classmethod def find_poetry_managed_pythons(cls) -> Iterator[Python]: finder = findpython.Finder( selected_providers=[PoetryPythonPathProvider.name()], ) for python in finder.find_all(): yield cls(python=python) @classmethod def find_all_versions( cls, constraint: VersionConstraint | str | None = None, implementation: str | None = None, free_threaded: bool | None = None, ) -> Iterator[PythonInfo]: if isinstance(constraint, str): constraint = parse_constraint(constraint) constraint = constraint or parse_constraint("*") if implementation: implementation = implementation.lower() seen = set() for python in cls.find_all(): if ( python.executable in seen or not constraint.allows(python.version) or (implementation and python.implementation.lower() != implementation) or ( free_threaded is not None and python.free_threaded is not free_threaded ) ): continue seen.add(python.executable) yield PythonInfo( major=python.major, minor=python.minor, patch=python.patch, implementation=python.implementation.lower(), free_threaded=python.free_threaded, executable=python.executable, ) @classmethod def find_downloadable_versions( cls, constraint: VersionConstraint | str | None = None, *, include_incompatible: bool = False, ) -> Iterator[PythonInfo]: if isinstance(constraint, str): constraint = parse_constraint(constraint) constraint = constraint or parse_constraint("*") for pv in PYTHON_VERSIONS: for _ in { k[1] for k in PYTHON_VERSIONS[pv] if include_incompatible or (k[0], k[1]) == (THIS_PLATFORM, THIS_ARCH) }: if not constraint.allows( Version.from_parts(pv.major, pv.minor, pv.micro) ): continue yield PythonInfo( major=pv.major, minor=pv.minor, patch=pv.micro, implementation=pv.implementation.lower(), free_threaded=pv.freethreaded, executable=None, ) @property def python(self) -> findpython.PythonVersion: return self._python @property def name(self) -> str: return cast("str", self._python.name) @property def executable(self) -> Path: return cast("Path", self._python.interpreter) @property def implementation(self) -> str: return cast("str", self._python.implementation.lower()) @property def free_threaded(self) -> bool: return cast("bool", self._python.freethreaded) @property def major(self) -> int: return cast("int", self._python.major) @property def minor(self) -> int: return cast("int", self._python.minor) @property def patch(self) -> int: return cast("int", self._python.patch) @property def version(self) -> Version: return Version.parse(str(self._python.version)) @cached_property def patch_version(self) -> Version: return Version.from_parts( major=self.version.major, minor=self.version.minor, patch=self.version.patch, ) @cached_property def minor_version(self) -> Version: return Version.from_parts(major=self.version.major, minor=self.version.minor) @classmethod def get_active_python(cls) -> Python | None: """ Fetches the active Python interpreter from available system paths or falls back to finding the first valid Python executable named "python". An "active Python interpreter" in this context is an executable (or a symlink) with the name `python`. This is done so to detect cases where pyenv or alternatives are used. This method first uses the `ShutilWhichPythonProvider` to detect Python executables in the path. If no interpreter is found using, it attempts to locate a Python binary named "python" via the `findpython` library. :return: An instance representing the detected active Python, or None if no valid environment is found. """ for python in ShutilWhichPythonProvider().find_pythons(): return cls(python=python) # fallback to findpython, restrict to finding only executables # named "python" as the intention here is just that, nothing more if python := findpython.find("python"): return cls(python=python) return None @classmethod def get_system_python(cls) -> Python: """ Creates and returns an instance of the class representing the Poetry's Python executable. """ return cls( python=findpython.PythonVersion( executable=Path(sys.executable), _version=packaging.version.Version( ".".join(str(v) for v in sys.version_info[:3]) ), ) ) @classmethod def get_by_name(cls, python_name: str) -> Python | None: # Ignore broken installations. with contextlib.suppress(ValueError): if python := ShutilWhichPythonProvider.find_python_by_name(python_name): return cls(python=python) if python := findpython.find(python_name): return cls(python=python) return None @classmethod def get_preferred_python(cls, config: Config, io: IO | None = None) -> Python: """ Determine and return the "preferred" Python interpreter based on the provided configuration and optional input/output stream. This method first attempts to get the active Python interpreter if the configuration does not mandate using Poetry's Python. If an active interpreter is found, it is returned. Otherwise, the method defaults to retrieving the Poetry's Python interpreter (System Python). This method **does not** attempt to sort versions or determine Python version constraint compatibility. """ io = io or NullIO() if not config.get("virtualenvs.use-poetry-python") and ( active_python := Python.get_active_python() ): io.write_error_line( f"Found: {active_python.executable}", verbosity=Verbosity.VERBOSE ) return active_python return cls.get_system_python() @classmethod def get_compatible_python(cls, poetry: Poetry, io: IO | None = None) -> Python: """ Retrieve a compatible Python version based on the given poetry configuration and Python constraints derived from the project. This method iterates through all available Python candidates and checks if any match the supported Python constraint as defined in the specified poetry package. :param poetry: The poetry configuration containing package information, including Python constraints. :param io: The input/output stream for error and status messages. Defaults to a null I/O if not provided. :return: A Python instance representing a compatible Python version. :raises NoCompatiblePythonVersionFoundError: If no Python version matches the supported constraint. """ io = io or NullIO() supported_python = poetry.package.python_constraint for python in cls.find_all(): if python.version.allows_any(supported_python): io.write_error_line( f"Using {python.name} ({python.patch_version})" ) return python raise NoCompatiblePythonVersionFoundError(poetry.package.python_versions) ================================================ FILE: src/poetry/utils/env/python/providers.py ================================================ from __future__ import annotations import dataclasses import shutil import sysconfig from pathlib import Path from typing import TYPE_CHECKING import findpython from findpython.providers.path import PathProvider from poetry.config.config import Config from poetry.utils._compat import WINDOWS if TYPE_CHECKING: from collections.abc import Iterable from poetry.core.constraints.version import Version from typing_extensions import Self class ShutilWhichPythonProvider(findpython.BaseProvider): # type: ignore[misc] @classmethod def create(cls) -> Self | None: return cls() def find_pythons(self) -> Iterable[findpython.PythonVersion]: if python := self.find_python_by_name("python"): return [python] return [] @classmethod def find_python_by_name(cls, name: str) -> findpython.PythonVersion | None: if path := shutil.which(name): return findpython.PythonVersion(executable=Path(path)) return None @dataclasses.dataclass class PoetryPythonPathProvider(PathProvider): # type: ignore[misc] @classmethod def installation_dir( cls, version: Version, implementation: str, free_threaded: bool ) -> Path: dir_name = f"{implementation}@{version}" if free_threaded: dir_name += "t" return Config.create().python_installation_dir / dir_name @classmethod def _make_bin_paths(cls, base: Path | None = None) -> list[Path]: # Attention: # There are two versions of pbs builds, # - one like a normal Python installation and # - one with an additional level of folders where the expected files # are in an "install" directory. # If both versions exist, the first one is preferred. # However, sometimes (especially for free-threaded Python), # only the second version exists! install_dir = base or Config.create().python_installation_dir if WINDOWS and not sysconfig.get_platform().startswith("mingw"): # On Windows Python executables are top level. # (Only in virtualenvs, they are in the Scripts directory.) # A python-build-standalone PyPy has no Scripts directory! if base: if not base.is_dir(): return [] if (install_dir := base / "install").is_dir(): return [install_dir] return [base] return [ *( pi if (pi := p / "install").exists() else p for p in Path.glob(install_dir, "*") if p.is_dir() ), ] return list(Path.glob(install_dir, "**/bin")) @classmethod def installation_bin_paths( cls, version: Version, implementation: str, free_threaded: bool = False ) -> list[Path]: return cls._make_bin_paths( cls.installation_dir(version, implementation, free_threaded) ) @classmethod def create(cls) -> Self | None: return cls(cls._make_bin_paths()) ================================================ FILE: src/poetry/utils/env/script_strings.py ================================================ from __future__ import annotations import packaging.tags GET_PLATFORMS = f""" import importlib.util import json import sys from pathlib import Path spec = importlib.util.spec_from_file_location( "packaging", Path(r"{packaging.__file__}") ) packaging = importlib.util.module_from_spec(spec) sys.modules[spec.name] = packaging spec = importlib.util.spec_from_file_location( "packaging.tags", Path(r"{packaging.tags.__file__}") ) packaging_tags = importlib.util.module_from_spec(spec) spec.loader.exec_module(packaging_tags) print(json.dumps(list(packaging_tags.platform_tags()))) """ GET_ENVIRONMENT_INFO = """\ import json import os import platform import sys import sysconfig INTERPRETER_SHORT_NAMES = { "python": "py", "cpython": "cp", "pypy": "pp", "ironpython": "ip", "jython": "jy", } def interpreter_version(): version = sysconfig.get_config_var("interpreter_version") if version: version = str(version) else: version = _version_nodot(sys.version_info[:2]) return version def _version_nodot(version): return "".join(map(str, version)) if hasattr(sys, "implementation"): info = sys.implementation.version iver = "{0.major}.{0.minor}.{0.micro}".format(info) kind = info.releaselevel if kind != "final": iver += kind[0] + str(info.serial) implementation_name = sys.implementation.name else: iver = "0" implementation_name = platform.python_implementation().lower() env = { "implementation_name": implementation_name, "implementation_version": iver, "os_name": os.name, "platform_machine": platform.machine(), "platform_release": platform.release(), "platform_system": platform.system(), "platform_version": platform.version(), "python_full_version": platform.python_version().rstrip("+"), "platform_python_implementation": platform.python_implementation(), "python_version": ".".join(platform.python_version_tuple()[:2]), "sys_platform": sys.platform, "version_info": tuple(sys.version_info), # Extra information "interpreter_name": INTERPRETER_SHORT_NAMES.get( implementation_name, implementation_name ), "interpreter_version": interpreter_version(), "sysconfig_platform": sysconfig.get_platform(), "free_threading": bool(sysconfig.get_config_var("Py_GIL_DISABLED")), } print(json.dumps(env)) """ GET_BASE_PREFIX = """\ import sys if hasattr(sys, "real_prefix"): print(sys.real_prefix) elif hasattr(sys, "base_prefix"): print(sys.base_prefix) else: print(sys.prefix) """ GET_PYTHON_VERSION_ONELINER = ( "import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))" ) GET_ENV_PATH_ONELINER = "import sys; print(sys.prefix)" GET_SYS_PATH = """\ import json import sys print(json.dumps(sys.path)) """ GET_PATHS = """\ import json import site import sysconfig paths = sysconfig.get_paths().copy() paths["fallbacks"] = [ p for p in site.getsitepackages() if p and p not in {paths.get("purelib"), paths.get("platlib")} ] if site.check_enableusersite(): paths["usersite"] = site.getusersitepackages() paths["userbase"] = site.getuserbase() print(json.dumps(paths)) """ ================================================ FILE: src/poetry/utils/env/site_packages.py ================================================ from __future__ import annotations import contextlib import itertools from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Literal from typing import overload from poetry.utils.helpers import is_dir_writable from poetry.utils.helpers import paths_csv from poetry.utils.helpers import remove_directory if TYPE_CHECKING: from collections.abc import Iterable class SitePackages: def __init__( self, purelib: Path, platlib: Path | None = None, fallbacks: list[Path] | None = None, ) -> None: self._purelib = purelib self._platlib = platlib or purelib if platlib and platlib.resolve() == purelib.resolve(): self._platlib = purelib self._fallbacks = fallbacks or [] self._candidates: list[Path] = [] for path in itertools.chain([self._purelib, self._platlib], self._fallbacks): if path not in self._candidates: self._candidates.append(path) self._writable_candidates: list[Path] | None = None @property def path(self) -> Path: return self._purelib @property def purelib(self) -> Path: return self._purelib @property def platlib(self) -> Path: return self._platlib @property def candidates(self) -> list[Path]: return self._candidates @property def writable_candidates(self) -> list[Path]: if self._writable_candidates is not None: return self._writable_candidates self._writable_candidates = [] for candidate in self._candidates: if not is_dir_writable(path=candidate, create=True): continue self._writable_candidates.append(candidate) return self._writable_candidates def make_candidates( self, path: Path, writable_only: bool = False, strict: bool = False ) -> list[Path]: candidates = self._candidates if not writable_only else self.writable_candidates if path.is_absolute(): for candidate in candidates: with contextlib.suppress(ValueError): path.relative_to(candidate) return [path] site_type = "writable " if writable_only else "" raise ValueError( f"{path} is not relative to any discovered {site_type}sites" ) results = [candidate / path for candidate in candidates] if not results and strict: raise RuntimeError( f'Unable to find a suitable destination for "{path}" in' f" {paths_csv(self._candidates)}" ) return results def distributions( self, name: str | None = None, writable_only: bool = False ) -> Iterable[metadata.Distribution]: path = list( map( str, self._candidates if not writable_only else self.writable_candidates ) ) yield from metadata.PathDistribution.discover(name=name, path=path) def find_distribution( self, name: str, writable_only: bool = False ) -> metadata.Distribution | None: for distribution in self.distributions(name=name, writable_only=writable_only): return distribution return None def find_distribution_files_with_name( self, distribution_name: str, name: str, writable_only: bool = False ) -> Iterable[Path]: for distribution in self.distributions( name=distribution_name, writable_only=writable_only ): files = [] if distribution.files is None else distribution.files for file in files: if file.name == name: path = distribution.locate_file(file) assert isinstance(path, Path) yield path def find_distribution_direct_url_json_files( self, distribution_name: str, writable_only: bool = False ) -> Iterable[Path]: return self.find_distribution_files_with_name( distribution_name=distribution_name, name="direct_url.json", writable_only=writable_only, ) def remove_distribution_files(self, distribution_name: str) -> list[Path]: paths = [] for distribution in self.distributions( name=distribution_name, writable_only=True ): files = [] if distribution.files is None else distribution.files for file in files: path = distribution.locate_file(file) assert isinstance(path, Path) path.unlink(missing_ok=True) distribution_path: Path = distribution._path # type: ignore[attr-defined] if distribution_path.exists(): remove_directory(distribution_path, force=True) paths.append(distribution_path) return paths @overload def _path_method_wrapper( self, path: Path, method: str, *args: Any, return_first: Literal[False], writable_only: bool = False, **kwargs: Any, ) -> list[tuple[Path, Any]]: ... @overload def _path_method_wrapper( self, path: Path, method: str, *args: Any, return_first: bool = True, writable_only: bool = False, **kwargs: Any, ) -> tuple[Path, Any]: ... def _path_method_wrapper( self, path: Path, method: str, *args: Any, return_first: bool = True, writable_only: bool = False, **kwargs: Any, ) -> tuple[Path, Any] | list[tuple[Path, Any]]: candidates = self.make_candidates( path, writable_only=writable_only, strict=True ) results = [] for candidate in candidates: with contextlib.suppress(OSError): result = candidate, getattr(candidate, method)(*args, **kwargs) if return_first: return result results.append(result) if results: return results raise OSError(f"Unable to access any of {paths_csv(candidates)}") def write_text(self, path: Path, *args: Any, **kwargs: Any) -> Path: paths: tuple[Path, Any] = self._path_method_wrapper( path, "write_text", *args, **kwargs ) return paths[0] def mkdir(self, path: Path, *args: Any, **kwargs: Any) -> Path: paths: tuple[Path, Any] = self._path_method_wrapper( path, "mkdir", *args, **kwargs ) return paths[0] def exists(self, path: Path) -> bool: return any( value[-1] for value in self._path_method_wrapper(path, "exists", return_first=False) ) def find( self, path: Path, writable_only: bool = False, ) -> list[Path]: return [ value[0] for value in self._path_method_wrapper( path, "exists", return_first=False, writable_only=writable_only ) if value[-1] is True ] ================================================ FILE: src/poetry/utils/env/system_env.py ================================================ from __future__ import annotations import os import platform import site import sys import sysconfig from pathlib import Path from packaging.tags import Tag from packaging.tags import interpreter_name from packaging.tags import interpreter_version from packaging.tags import sys_tags from poetry.utils.env.base_env import Env from poetry.utils.env.base_env import MarkerEnv class SystemEnv(Env): """ A system (i.e. not a virtualenv) Python environment. """ @property def python(self) -> Path: return Path(sys.executable) @property def sys_path(self) -> list[str]: return sys.path def get_paths(self) -> dict[str, str]: import site paths = sysconfig.get_paths().copy() if site.check_enableusersite(): paths["usersite"] = site.getusersitepackages() paths["userbase"] = site.getuserbase() return paths def get_supported_tags(self) -> list[Tag]: return list(sys_tags()) def get_marker_env(self) -> MarkerEnv: if hasattr(sys, "implementation"): info = sys.implementation.version iver = f"{info.major}.{info.minor}.{info.micro}" kind = info.releaselevel if kind != "final": iver += kind[0] + str(info.serial) implementation_name = sys.implementation.name else: iver = "0" implementation_name = "" return { "implementation_name": implementation_name, "implementation_version": iver, "os_name": os.name, "platform_machine": platform.machine(), "platform_release": platform.release(), "platform_system": platform.system(), "platform_version": platform.version(), # Workaround for https://github.com/python/cpython/issues/99968 "python_full_version": platform.python_version().rstrip("+"), "platform_python_implementation": platform.python_implementation(), "python_version": ".".join(platform.python_version().split(".")[:2]), "sys_platform": sys.platform, "version_info": sys.version_info, "interpreter_name": interpreter_name(), "interpreter_version": interpreter_version(), "sysconfig_platform": sysconfig.get_platform(), "free_threading": bool(sysconfig.get_config_var("Py_GIL_DISABLED")), } def is_venv(self) -> bool: return self._path != self._base def _get_lib_dirs(self) -> list[Path]: return super()._get_lib_dirs() + [Path(d) for d in site.getsitepackages()] ================================================ FILE: src/poetry/utils/env/virtual_env.py ================================================ from __future__ import annotations import json import os import re import sysconfig from contextlib import contextmanager from copy import deepcopy from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING from typing import Any from poetry.utils.env.base_env import Env from poetry.utils.env.base_env import MarkerEnv from poetry.utils.env.script_strings import GET_BASE_PREFIX from poetry.utils.env.script_strings import GET_ENVIRONMENT_INFO from poetry.utils.env.script_strings import GET_PATHS from poetry.utils.env.script_strings import GET_PLATFORMS from poetry.utils.env.script_strings import GET_SYS_PATH if TYPE_CHECKING: from collections.abc import Iterator from packaging.tags import Tag class VirtualEnv(Env): """ A virtual Python environment. """ def __init__(self, path: Path, base: Path | None = None) -> None: super().__init__(path, base) # If base is None, it probably means this is # a virtualenv created from VIRTUAL_ENV. # In this case we need to get sys.base_prefix # from inside the virtualenv. if base is None: output = self.run_python_script(GET_BASE_PREFIX) self._base = Path(output.strip()) @property def sys_path(self) -> list[str]: output = self.run_python_script(GET_SYS_PATH) paths: list[str] = json.loads(output) return paths def get_supported_tags(self) -> list[Tag]: from packaging.tags import compatible_tags from packaging.tags import cpython_tags from packaging.tags import generic_tags python = self.version_info[:3] interpreter_name = self.marker_env["interpreter_name"] interpreter_version = self.marker_env["interpreter_version"] sysconfig_platform = self.marker_env["sysconfig_platform"] free_threading = self.marker_env["free_threading"] abis: list[str] | None = None if interpreter_name == "pp": interpreter = "pp3" elif interpreter_name == "cp": interpreter = f"{interpreter_name}{interpreter_version}" if free_threading: abis = [f"{interpreter}t"] else: interpreter = None # Why using sysconfig.get_platform() and not ... # ... platform.machine() # This one is also different for x86_64 Linux and aarch64 Linux, # but it is the same for a 32 Bit and a 64 Bit Python on Windows! # ... platform.architecture() # This one is also different for a 32 Bit and a 64 Bit Python on Windows, # but it is the same for x86_64 Linux and aarch64 Linux! platforms = None if sysconfig_platform != sysconfig.get_platform(): # Relevant for the following use cases, for example: # - using a 32 Bit Python on a 64 Bit Windows # - using an emulated aarch Python on an x86_64 Linux output = self.run_python_script(GET_PLATFORMS) platforms = json.loads(output) return [ *( cpython_tags(python, abis=abis, platforms=platforms) if interpreter_name == "cp" else generic_tags(platforms=platforms) ), *compatible_tags(python, interpreter=interpreter, platforms=platforms), ] def get_marker_env(self) -> MarkerEnv: output = self.run_python_script(GET_ENVIRONMENT_INFO) env: MarkerEnv = json.loads(output) # Lists and tuples are the same in JSON and loaded as list. env["version_info"] = tuple(env["version_info"]) # type: ignore[typeddict-item] return env def get_paths(self) -> dict[str, str]: output = self.run_python_script(GET_PATHS) paths: dict[str, str] = json.loads(output) return paths def is_venv(self) -> bool: return True def is_sane(self) -> bool: # A virtualenv is considered sane if "python" exists. return os.path.exists(self.python) def _run(self, cmd: list[str], **kwargs: Any) -> str: kwargs["env"] = self.get_temp_environ(environ=kwargs.get("env")) return super()._run(cmd, **kwargs) def get_temp_environ( self, environ: dict[str, str] | None = None, exclude: list[str] | None = None, **kwargs: str, ) -> dict[str, str]: exclude = exclude or [] exclude.extend(["PYTHONHOME", "__PYVENV_LAUNCHER__"]) if environ: environ = deepcopy(environ) for key in exclude: environ.pop(key, None) else: environ = {k: v for k, v in os.environ.items() if k not in exclude} environ.update(kwargs) environ["PATH"] = self._updated_path() environ["VIRTUAL_ENV"] = str(self._path) return environ def execute(self, bin: str, *args: str, **kwargs: Any) -> int: kwargs["env"] = self.get_temp_environ(environ=kwargs.get("env")) return super().execute(bin, *args, **kwargs) @contextmanager def temp_environ(self) -> Iterator[None]: environ = dict(os.environ) try: yield finally: os.environ.clear() os.environ.update(environ) def _updated_path(self) -> str: return os.pathsep.join([str(self._bin_dir), os.environ.get("PATH", "")]) @cached_property def includes_system_site_packages(self) -> bool: pyvenv_cfg = self._path / "pyvenv.cfg" return pyvenv_cfg.exists() and ( re.search( r"^\s*include-system-site-packages\s*=\s*true\s*$", pyvenv_cfg.read_text(encoding="utf-8"), re.IGNORECASE | re.MULTILINE, ) is not None ) def is_path_relative_to_lib(self, path: Path) -> bool: return super().is_path_relative_to_lib(path) or ( self.includes_system_site_packages and self.parent_env.is_path_relative_to_lib(path) ) ================================================ FILE: src/poetry/utils/extras.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Collection from collections.abc import Iterable from collections.abc import Mapping from packaging.utils import NormalizedName from poetry.core.packages.package import Package def get_extra_package_names( packages: Iterable[Package], extras: Mapping[NormalizedName, Iterable[NormalizedName]], extra_names: Collection[NormalizedName], ) -> set[NormalizedName]: """ Returns all package names required by the given extras. :param packages: A collection of packages, such as from Repository.packages :param extras: A mapping of `extras` names to lists of package names, as defined in the `extras` section of `poetry.lock`. :param extra_names: A list of strings specifying names of extra groups to resolve. """ if not extra_names: return set() # lookup for packages by name, faster than looping over packages repeatedly packages_by_name = {package.name: package for package in packages} # Depth-first search, with our entry points being the packages directly required by # extras. seen_package_names = set() stack = [ extra_package_name for extra_name in extra_names for extra_package_name in extras.get(extra_name, ()) ] while stack: package_name = stack.pop() # We expect to find all packages, but can just carry on if we don't. package = packages_by_name.get(package_name) if package is None or package.name in seen_package_names: continue seen_package_names.add(package.name) stack += [dependency.name for dependency in package.requires] return seen_package_names ================================================ FILE: src/poetry/utils/helpers.py ================================================ from __future__ import annotations import hashlib import io import logging import os import shutil import stat import sys import tarfile import tempfile import zipfile from collections.abc import Mapping from contextlib import contextmanager from contextlib import suppress from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import overload from requests.exceptions import ChunkedEncodingError from requests.exceptions import ConnectionError from requests.utils import atomic_open from poetry.utils.authenticator import get_default_authenticator from poetry.utils.constants import REQUESTS_TIMEOUT if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Collection from collections.abc import Iterator from types import TracebackType from poetry.core.packages.package import Package from requests import Response from requests import Session from poetry.utils.authenticator import Authenticator logger = logging.getLogger(__name__) prioritised_hash_types: tuple[str, ...] = tuple( t for t in [ "sha3_512", "sha3_384", "sha3_256", "sha3_224", "sha512", "sha384", "sha256", "sha224", "shake_256", "shake_128", "blake2s", "blake2b", ] if t in hashlib.algorithms_available ) non_prioritised_available_hash_types: frozenset[str] = frozenset( set(hashlib.algorithms_available).difference(prioritised_hash_types) ) @contextmanager def directory(path: Path) -> Iterator[Path]: cwd = Path.cwd() try: os.chdir(path) yield path finally: os.chdir(cwd) # Correct type signature when used as `shutil.rmtree(..., onexc=_on_rm_error)`. @overload def _on_rm_error( func: Callable[[str], None], path: str, exc_info: Exception ) -> None: ... # Correct type signature when used as `shutil.rmtree(..., onerror=_on_rm_error)`. @overload def _on_rm_error( func: Callable[[str], None], path: str, exc_info: tuple[type[BaseException], BaseException, TracebackType], ) -> None: ... def _on_rm_error(func: Callable[[str], None], path: str, exc_info: Any) -> None: if not os.path.exists(path): return os.chmod(path, stat.S_IWRITE) func(path) def remove_directory(path: Path, force: bool = False) -> None: """ Helper function handle safe removal, and optionally forces stubborn file removal. This is particularly useful when dist files are read-only or git writes read-only files on Windows. Internally, all arguments are passed to `shutil.rmtree`. """ if path.is_symlink(): return os.unlink(path) kwargs: dict[str, Any] = {} if force: onexc = "onexc" if sys.version_info >= (3, 12) else "onerror" kwargs[onexc] = _on_rm_error shutil.rmtree(path, **kwargs) def merge_dicts(d1: dict[str, Any], d2: dict[str, Any]) -> None: for k in d2: if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], Mapping): merge_dicts(d1[k], d2[k]) else: d1[k] = d2[k] class HTTPRangeRequestSupportedError(Exception): """Raised when server unexpectedly supports byte ranges.""" def download_file( url: str, dest: Path, *, session: Authenticator | Session | None = None, chunk_size: int = 1024, raise_accepts_ranges: bool = False, max_retries: int = 0, ) -> None: from poetry.puzzle.provider import Indicator downloader = Downloader(url, dest, session, max_retries=max_retries) if raise_accepts_ranges and downloader.accepts_ranges: raise HTTPRangeRequestSupportedError(f"URL {url} supports range requests.") set_indicator = False with Indicator.context() as update_context: update_context(f"Downloading {url}") total_size = downloader.total_size if total_size > 0: fetched_size = 0 last_percent = 0 # if less than 1MB, we simply show that we're downloading # but skip the updating set_indicator = total_size > 1024 * 1024 for fetched_size in downloader.download_with_progress(chunk_size): if set_indicator: percent = (fetched_size * 100) // total_size if percent > last_percent: last_percent = percent update_context(f"Downloading {url} {percent:3}%") class Downloader: def __init__( self, url: str, dest: Path, session: Authenticator | Session | None = None, max_retries: int = 0, ): self._dest = dest self._max_retries = max_retries self._session = session or get_default_authenticator() self._url = url self._response = self._get() @cached_property def accepts_ranges(self) -> bool: return self._response.headers.get("Accept-Ranges") == "bytes" @cached_property def total_size(self) -> int: total_size = 0 if "Content-Length" in self._response.headers: with suppress(ValueError): total_size = int(self._response.headers["Content-Length"]) return total_size def _get(self, start: int = 0) -> Response: headers = {"Accept-Encoding": "Identity"} if start > 0: headers["Range"] = f"bytes={start}-" response = self._session.get( self._url, stream=True, headers=headers, timeout=REQUESTS_TIMEOUT ) try: response.raise_for_status() return response except BaseException: response.close() raise def _iter_content_with_resume(self, chunk_size: int) -> Iterator[bytes]: fetched_size = 0 retries = 0 while True: try: with self._response: for chunk in self._response.iter_content(chunk_size=chunk_size): yield chunk fetched_size += len(chunk) except (ChunkedEncodingError, ConnectionError): if ( retries < self._max_retries and self.accepts_ranges and fetched_size > 0 ): # only retry if server supports byte ranges # and we have fetched at least one chunk # otherwise, we should just fail retries += 1 self._response = self._get(fetched_size) continue raise else: break def download_with_progress(self, chunk_size: int = 1024) -> Iterator[int]: fetched_size = 0 with atomic_open(self._dest) as f: for chunk in self._iter_content_with_resume(chunk_size=chunk_size): if chunk: f.write(chunk) fetched_size += len(chunk) yield fetched_size def get_package_version_display_string( package: Package, root: Path | None = None ) -> str: if package.source_type in ["file", "directory"] and root: assert package.source_url is not None path = Path(os.path.relpath(package.source_url, root)).as_posix() return f"{package.version} {path}" pretty_version: str = package.full_pretty_version return pretty_version def paths_csv(paths: list[Path]) -> str: return ", ".join(f'"{c!s}"' for c in paths) def ensure_path(path: str | Path, is_directory: bool = False) -> Path: if isinstance(path, str): path = Path(path) if path.exists() and path.is_dir() == is_directory: return path raise ValueError( f"Specified path '{path}' is not a valid {'directory' if is_directory else 'file'}." ) def is_dir_writable(path: Path, create: bool = False) -> bool: try: if not path.exists(): if not create: return False path.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryFile(dir=str(path)): pass except OSError: return False else: return True def pluralize(count: int, word: str = "") -> str: if count == 1: return word return word + "s" def _get_win_folder_from_registry(csidl_name: str) -> str: if sys.platform != "win32": raise RuntimeError("Method can only be called on Windows.") import winreg as _winreg shell_folder_name = { "CSIDL_APPDATA": "AppData", "CSIDL_COMMON_APPDATA": "Common AppData", "CSIDL_LOCAL_APPDATA": "Local AppData", "CSIDL_PROGRAM_FILES": "Program Files", }[csidl_name] key = _winreg.OpenKey( _winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", ) dir, _type = _winreg.QueryValueEx(key, shell_folder_name) assert isinstance(dir, str) return dir def _get_win_folder_with_ctypes(csidl_name: str) -> str: if sys.platform != "win32": raise RuntimeError("Method can only be called on Windows.") import ctypes csidl_const = { "CSIDL_APPDATA": 26, "CSIDL_COMMON_APPDATA": 35, "CSIDL_LOCAL_APPDATA": 28, "CSIDL_PROGRAM_FILES": 38, }[csidl_name] buf = ctypes.create_unicode_buffer(1024) ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) # Downgrade to short path name if have highbit chars. See # . has_high_char = False for c in buf: if ord(c) > 255: has_high_char = True break if has_high_char: buf2 = ctypes.create_unicode_buffer(1024) if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): buf = buf2 return buf.value def get_win_folder(csidl_name: str) -> Path: if sys.platform == "win32": try: from ctypes import windll # noqa: F401 _get_win_folder = _get_win_folder_with_ctypes except ImportError: _get_win_folder = _get_win_folder_from_registry return Path(_get_win_folder(csidl_name)) raise RuntimeError("Method can only be called on Windows.") def get_real_windows_path(path: Path) -> Path: program_files = get_win_folder("CSIDL_PROGRAM_FILES") local_appdata = get_win_folder("CSIDL_LOCAL_APPDATA") path = Path( str(path).replace( str(program_files / "WindowsApps"), str(local_appdata / "Microsoft/WindowsApps"), ) ) if path.as_posix().startswith(local_appdata.as_posix()): path = path.resolve() return path def get_file_hash(path: Path, hash_name: str = "sha256") -> str: h = hashlib.new(hash_name) with path.open("rb") as fp: for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""): h.update(content) return h.hexdigest() def get_highest_priority_hash_type( hash_types: Collection[str], archive_name: str ) -> str | None: if not hash_types: return None for prioritised_hash_type in prioritised_hash_types: if prioritised_hash_type in hash_types: return prioritised_hash_type logger.debug( f"There are no known hash types for {archive_name} that are prioritised (known" f" hash types: {hash_types!s})" ) for available_hash_type in non_prioritised_available_hash_types: if available_hash_type in hash_types: return available_hash_type return None def extractall(source: Path, dest: Path, zip: bool) -> None: """Extract all members from either a zip or tar archive.""" if zip: with zipfile.ZipFile(source) as archive: archive.extractall(dest) else: # These versions of python shipped with a broken tarfile data_filter, per # https://github.com/python/cpython/issues/107845. broken_tarfile_filter = {(3, 9, 17), (3, 10, 12), (3, 11, 4)} with tarfile.open(source) as archive: if ( hasattr(tarfile, "data_filter") and sys.version_info[:3] not in broken_tarfile_filter ): archive.extractall(dest, filter="data") else: archive.extractall(dest) ================================================ FILE: src/poetry/utils/isolated_build.py ================================================ from __future__ import annotations import os import subprocess from contextlib import contextmanager from contextlib import redirect_stdout from io import StringIO from typing import TYPE_CHECKING from build import BuildBackendException from build.env import IsolatedEnv as BaseIsolatedEnv from poetry.core.packages.dependency_group import DependencyGroup from poetry.utils._compat import decode from poetry.utils.env import Env from poetry.utils.env import EnvManager from poetry.utils.env import ephemeral_environment if TYPE_CHECKING: from collections.abc import Collection from collections.abc import Iterator from pathlib import Path from build import DistributionType from build import ProjectBuilder from poetry.core.packages.dependency import Dependency from poetry.repositories import RepositoryPool CONSTRAINTS_GROUP_NAME = "constraints" class IsolatedBuildBaseError(Exception): ... class IsolatedBuildBackendError(IsolatedBuildBaseError): def __init__(self, source: Path, exception: BuildBackendException) -> None: super().__init__() self.source = source self.exception = exception def generate_message( self, source_string: str | None = None, build_command: str | None = None ) -> str: e = self.exception.exception source_string = source_string or self.source.as_posix() build_command = ( build_command or f'pip wheel --no-cache-dir --use-pep517 "{self.source.as_posix()}"' ) reasons = ["PEP517 build of a dependency failed", str(self.exception)] if isinstance(e, subprocess.CalledProcessError): inner_traceback = decode(e.stderr or e.stdout or e.output).strip() inner_reason = "\n | ".join( ["", str(e), "", *inner_traceback.split("\n")] ).lstrip("\n") reasons.append(f"{inner_reason}") reasons.append( "" "Note: This error originates from the build backend, and is likely not a " f"problem with poetry but one of the following issues with {source_string}\n\n" " - not supporting PEP 517 builds\n" " - not specifying PEP 517 build requirements correctly\n" " - the build requirements are incompatible with your operating system or Python version\n" " - the build requirements are missing system dependencies (eg: compilers, libraries, headers).\n\n" f"You can verify this by running {build_command}." "" ) return "\n\n".join(reasons) def __str__(self) -> str: return self.generate_message() class IsolatedBuildInstallError(IsolatedBuildBaseError): def __init__(self, requirements: Collection[str], output: str, error: str) -> None: message = "\n\n".join( ( f"Failed to install {', '.join(requirements)}.", f"Output:\n{output}", f"Error:\n{error}", ) ) super().__init__(message) self._requirements = requirements @property def requirements(self) -> Collection[str]: return self._requirements class IsolatedEnv(BaseIsolatedEnv): def __init__(self, env: Env, pool: RepositoryPool) -> None: self._env = env self._pool = pool @property def python_executable(self) -> str: return str(self._env.python) def make_extra_environ(self) -> dict[str, str]: path = os.environ.get("PATH") scripts_dir = str(self._env._bin_dir) return { "PATH": ( os.pathsep.join([scripts_dir, path]) if path is not None else scripts_dir ) } def install( self, requirements: Collection[str], *, constraints: list[Dependency] | None = None, ) -> None: from cleo.io.buffered_io import BufferedIO from poetry.core.packages.dependency import Dependency from poetry.core.packages.project_package import ProjectPackage from poetry.config.config import Config from poetry.installation.installer import Installer from poetry.packages.locker import Locker from poetry.repositories.installed_repository import InstalledRepository # We build Poetry dependencies from the requirements package = ProjectPackage("__root__", "0.0.0") package.python_versions = ".".join(str(v) for v in self._env.version_info[:3]) env_markers = self._env.get_marker_env() for requirement in requirements: dependency = Dependency.create_from_pep_508(requirement) if dependency.marker.is_empty() or dependency.marker.validate(env_markers): # we ignore dependencies that are not valid for this environment # this ensures that we do not end up with unnecessary constraint # errors when solving build system requirements; this is assumed # safe as this environment is ephemeral package.add_dependency(dependency) if constraints: constraints_group = DependencyGroup(CONSTRAINTS_GROUP_NAME, optional=True) for constraint in constraints: if constraint.marker.validate(env_markers): constraints_group.add_dependency(constraint) package.add_dependency_group(constraints_group) io = BufferedIO() installer = Installer( io, self._env, package, Locker(self._env.path.joinpath("poetry.lock"), {}), self._pool, Config.create(), InstalledRepository.load(self._env), ) installer.update(True) if installer.run() != 0: raise IsolatedBuildInstallError( requirements, io.fetch_output(), io.fetch_error() ) @contextmanager def isolated_builder( source: Path, distribution: DistributionType = "wheel", python_executable: Path | None = None, pool: RepositoryPool | None = None, *, build_constraints: list[Dependency] | None = None, ) -> Iterator[ProjectBuilder]: from build import ProjectBuilder from pyproject_hooks import quiet_subprocess_runner from poetry.factory import Factory try: # we recreate the project's Poetry instance in order to retrieve the correct repository pool # when a pool is not provided pool = pool or Factory().create_poetry().pool except RuntimeError: # the context manager is not being called within a Poetry project context # fallback to a default pool using only PyPI as source from poetry.repositories import RepositoryPool from poetry.repositories.pypi_repository import PyPiRepository # fallback to using only PyPI pool = RepositoryPool(repositories=[PyPiRepository()]) python_executable = ( python_executable or EnvManager.get_system_env(naive=True).python ) with ephemeral_environment( executable=python_executable, flags={"no-pip": True}, ) as venv: env = IsolatedEnv(venv, pool) stdout = StringIO() try: builder = ProjectBuilder.from_isolated_env( env, source, runner=quiet_subprocess_runner ) with redirect_stdout(stdout): env.install( builder.build_system_requires, constraints=build_constraints ) # we repeat the build system requirements to avoid poetry installer from removing them env.install( builder.build_system_requires | builder.get_requires_for_build(distribution), constraints=build_constraints, ) yield builder except BuildBackendException as e: raise IsolatedBuildBackendError(source, e) from None ================================================ FILE: src/poetry/utils/log_utils.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from poetry.core.packages.package import Package from poetry.utils.env import Env def format_build_wheel_log(package: Package, env: Env) -> str: """Format a log message indicating that a wheel is being built for the given package and environment.""" marker_env = env.marker_env python_version_info = marker_env.get( "version_info", ("", "", "") ) python_version = ( f"{python_version_info[0]}.{python_version_info[1]}.{python_version_info[2]}" ) platform = marker_env.get("sys_platform", "") architecture = marker_env.get("platform_machine", "") message = ( f" Building a wheel file for {package.pretty_name} " f"for Python {python_version} on {platform}-{architecture}" ) return message ================================================ FILE: src/poetry/utils/password_manager.py ================================================ from __future__ import annotations import dataclasses import functools import logging from contextlib import suppress from typing import TYPE_CHECKING from poetry.config.config import Config from poetry.utils.threading import atomic_cached_property if TYPE_CHECKING: import keyring.backend from cleo.io.io import IO logger = logging.getLogger(__name__) class PoetryKeyringError(Exception): pass @dataclasses.dataclass class HTTPAuthCredential: username: str | None = dataclasses.field(default=None) password: str | None = dataclasses.field(default=None) class PoetryKeyring: # some private sources expect tokens to be provided as passwords with empty usernames # we use a fixed literal to ensure that this can be stored in keyring (jaraco/keyring#687) # # Note: If this is changed, users with passwords stored with empty usernames will have to # re-add the config. _EMPTY_USERNAME_KEY = "__poetry_source_empty_username__" def __init__(self, namespace: str) -> None: self._namespace = namespace @staticmethod def preflight_check(io: IO | None = None, config: Config | None = None) -> None: """ Performs a preflight check to determine the availability of the keyring service and logs the status if verbosity is enabled. This method is used to validate the configuration setup related to the keyring functionality. :param io: An optional input/output handler used to log messages during the preflight check. If not provided, logging will be skipped. :param config: An optional configuration object. If not provided, a new configuration instance will be created using the default factory method. :return: None """ config = config or Config.create() if config.get("keyring.enabled"): if io and io.is_verbose(): io.write("Checking keyring availability: ") message = "Unavailable" with suppress(RuntimeError, ValueError): if PoetryKeyring.is_available(): message = "Available" if io and io.is_verbose(): io.write(message) io.write_line("") def get_credential( self, *names: str, username: str | None = None ) -> HTTPAuthCredential: import keyring from keyring.errors import KeyringError from keyring.errors import KeyringLocked for name in names: credential = None try: # we do default to empty username string here since credentials support empty usernames credential = keyring.get_credential(name, username) except KeyringLocked: logger.debug("Keyring %s is locked", name) except (KeyringError, RuntimeError): logger.debug("Accessing keyring %s failed", name, exc_info=True) if credential: return HTTPAuthCredential( username=credential.username, password=credential.password ) return HTTPAuthCredential(username=username, password=None) def get_password(self, name: str, username: str) -> str | None: import keyring import keyring.errors name = self.get_entry_name(name) try: return keyring.get_password(name, username or self._EMPTY_USERNAME_KEY) except (RuntimeError, keyring.errors.KeyringError) as e: raise PoetryKeyringError( f"Unable to retrieve the password for {name} from the key ring {e}" ) def set_password(self, name: str, username: str, password: str) -> None: import keyring import keyring.errors name = self.get_entry_name(name) try: keyring.set_password(name, username or self._EMPTY_USERNAME_KEY, password) except (RuntimeError, keyring.errors.KeyringError) as e: raise PoetryKeyringError( f"Unable to store the password for {name} in the key ring: {e}" ) def delete_password(self, name: str, username: str) -> None: import keyring.errors name = self.get_entry_name(name) try: keyring.delete_password(name, username or self._EMPTY_USERNAME_KEY) except (RuntimeError, keyring.errors.KeyringError): raise PoetryKeyringError( f"Unable to delete the password for {name} from the key ring" ) def get_entry_name(self, name: str) -> str: return f"{self._namespace}-{name}" @classmethod @functools.cache def is_available(cls) -> bool: logger.debug("Checking if keyring is available") try: import keyring import keyring.backend import keyring.errors except ImportError as e: logger.debug("An error occurred while importing keyring: %s", e) return False def backend_name(backend: keyring.backend.KeyringBackend) -> str: name: str = backend.name return name.split(" ")[0] def backend_is_valid(backend: keyring.backend.KeyringBackend) -> bool: name = backend_name(backend) if name in ("chainer", "fail", "null"): logger.debug(f"Backend {backend.name!r} is not suitable") return False elif "plaintext" in backend.name.lower(): logger.debug(f"Not using plaintext keyring backend {backend.name!r}") return False return True backend = keyring.get_keyring() if backend_name(backend) == "chainer": backends = keyring.backend.get_all_keyring() valid_backend = next((b for b in backends if backend_is_valid(b)), None) else: valid_backend = backend if backend_is_valid(backend) else None if valid_backend is None: logger.debug("No valid keyring backend was found") return False logger.debug(f"Using keyring backend {backend.name!r}") try: # unfortunately there is no clean way of checking if keyring is unlocked keyring.get_password("python-poetry-check", "python-poetry") except (RuntimeError, keyring.errors.KeyringError): logger.debug( "Accessing keyring failed during availability check", exc_info=True ) return False return True class PasswordManager: def __init__(self, config: Config) -> None: self._config = config @atomic_cached_property def use_keyring(self) -> bool: return self._config.get("keyring.enabled") and PoetryKeyring.is_available() @atomic_cached_property def keyring(self) -> PoetryKeyring: if not self.use_keyring: raise PoetryKeyringError( "Access to keyring was requested, but it is not available" ) return PoetryKeyring("poetry-repository") @staticmethod def warn_plaintext_credentials_stored() -> None: logger.warning("Using a plaintext file to store credentials") def set_pypi_token(self, repo_name: str, token: str) -> None: if not self.use_keyring: self.warn_plaintext_credentials_stored() self._config.auth_config_source.add_property( f"pypi-token.{repo_name}", token ) else: self.keyring.set_password(repo_name, "__token__", token) def get_pypi_token(self, repo_name: str) -> str | None: """Get PyPi token. First checks the environment variables for a token, then the configured username/password and the available keyring. :param repo_name: Name of repository. :return: Returns a token as a string if found, otherwise None. """ token: str | None = self._config.get(f"pypi-token.{repo_name}") if token: return token if self.use_keyring: return self.keyring.get_password(repo_name, "__token__") else: return None def delete_pypi_token(self, repo_name: str) -> None: if not self.use_keyring: return self._config.auth_config_source.remove_property( f"pypi-token.{repo_name}" ) self.keyring.delete_password(repo_name, "__token__") def get_http_auth(self, repo_name: str) -> HTTPAuthCredential: username = self._config.get(f"http-basic.{repo_name}.username") password = self._config.get(f"http-basic.{repo_name}.password") if password is None and self.use_keyring: password = self.keyring.get_password(repo_name, username) # we use `or None` here to ensure that empty strings are passed as None return HTTPAuthCredential(username=username or None, password=password or None) def set_http_password(self, repo_name: str, username: str, password: str) -> None: auth = {"username": username} if not self.use_keyring: self.warn_plaintext_credentials_stored() auth["password"] = password else: self.keyring.set_password(repo_name, username, password) self._config.auth_config_source.add_property(f"http-basic.{repo_name}", auth) def delete_http_password(self, repo_name: str) -> None: auth = self.get_http_auth(repo_name) if auth.username is None: return with suppress(PoetryKeyringError): self.keyring.delete_password(repo_name, auth.username) self._config.auth_config_source.remove_property(f"http-basic.{repo_name}") def get_credential( self, *names: str, username: str | None = None ) -> HTTPAuthCredential: if self.use_keyring: return self.keyring.get_credential(*names, username=username) return HTTPAuthCredential(username=username, password=None) ================================================ FILE: src/poetry/utils/patterns.py ================================================ from __future__ import annotations import re wheel_file_re = re.compile( r"^(?P(?P.+?)-(?P\d[^-]*))" r"(-(?P\d[^-]*))?" r"-(?P[^-]+)" r"-(?P[^-]+)" r"-(?P[^-]+)" r"\.whl$", re.VERBOSE, ) sdist_file_re = re.compile( r"^(?P(?P.+?)-(?P\d[^-]*?))" r"(\.sdist)?\.(?P(zip|tar(\.(gz|bz2|xz|Z))?))$" ) ================================================ FILE: src/poetry/utils/pip.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.exceptions import PoetryError from poetry.utils.env import EnvCommandError if TYPE_CHECKING: from pathlib import Path from poetry.utils.env import Env def pip_install( path: Path, environment: Env, editable: bool = False, deps: bool = False, upgrade: bool = False, ) -> str: is_wheel = path.suffix == ".whl" # We disable version check here as we are already pinning to version available in # either the virtual environment or the virtualenv package embedded wheel. Version # checks are a wasteful network call that adds a lot of wait time when installing a # lot of packages. args = [ "install", "--disable-pip-version-check", "--isolated", "--no-input", "--prefix", str(environment.path), ] if not is_wheel and not editable: args.insert(1, "--use-pep517") if upgrade: args.append("--upgrade") if not deps: args.append("--no-deps") if editable: if not path.is_dir(): raise PoetryError( "Cannot install non directory dependencies in editable mode" ) args.append("-e") args.append(str(path)) try: return environment.run_pip(*args) except EnvCommandError as e: raise PoetryError(f"Failed to install {path}") from e ================================================ FILE: src/poetry/utils/threading.py ================================================ from __future__ import annotations import functools import threading from typing import TYPE_CHECKING from typing import TypeVar from typing import overload from weakref import WeakKeyDictionary if TYPE_CHECKING: from collections.abc import Callable from typing import Any T = TypeVar("T") C = TypeVar("C", bound=object) class AtomicCachedProperty(functools.cached_property[T]): def __init__(self, func: Callable[[C], T]) -> None: super().__init__(func) self._semaphore = threading.BoundedSemaphore() self._locks: WeakKeyDictionary[object, threading.Lock] = WeakKeyDictionary() @overload def __get__( self, instance: None, owner: type[Any] | None = ... ) -> AtomicCachedProperty[T]: ... @overload def __get__(self, instance: object, owner: type[Any] | None = ...) -> T: ... def __get__( self, instance: C | None, owner: type[Any] | None = None ) -> AtomicCachedProperty[T] | T: # If there's no instance, return the descriptor itself if instance is None: return self if instance not in self._locks: with self._semaphore: # we double-check the lock has not been created by another thread if instance not in self._locks: self._locks[instance] = threading.Lock() # Use a thread-safe lock to ensure the property is computed only once with self._locks[instance]: return super().__get__(instance, owner) def atomic_cached_property(func: Callable[[C], T]) -> AtomicCachedProperty[T]: """ A thread-safe implementation of functools.cached_property that ensures lazily-computed properties are calculated only once, even in multithreaded environments. This property decorator works similar to functools.cached_property but employs thread locks and a bounded semaphore to handle concurrent access safely. The computed value is cached on the instance itself and is reused for subsequent accesses unless explicitly invalidated. The added thread-safety makes it ideal for situations where multiple threads might access and compute the property simultaneously. Note: - The cache is stored in the instance dictionary just like `functools.cached_property`. :param func: The function to be turned into a thread-safe cached property. """ return AtomicCachedProperty(func) ================================================ FILE: src/poetry/utils/wheel.py ================================================ from __future__ import annotations import logging from typing import TYPE_CHECKING from packaging.tags import Tag from poetry.utils.patterns import wheel_file_re if TYPE_CHECKING: from poetry.utils.env import Env logger = logging.getLogger(__name__) class InvalidWheelNameError(Exception): pass class Wheel: def __init__(self, filename: str) -> None: wheel_info = wheel_file_re.match(filename) if not wheel_info: raise InvalidWheelNameError(f"{filename} is not a valid wheel filename.") self.filename = filename self.name = wheel_info.group("name").replace("_", "-") self.version = wheel_info.group("ver").replace("_", "-") self.build_tag = wheel_info.group("build") self.pyversions = wheel_info.group("pyver").split(".") self.abis = wheel_info.group("abi").split(".") self.plats = wheel_info.group("plat").split(".") self.tags = { Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats } def get_minimum_supported_index(self, tags: list[Tag]) -> int | None: indexes = [tags.index(t) for t in self.tags if t in tags] return min(indexes) if indexes else None def is_supported_by_environment(self, env: Env) -> bool: return bool(set(env.supported_tags).intersection(self.tags)) ================================================ FILE: src/poetry/vcs/__init__.py ================================================ ================================================ FILE: src/poetry/vcs/git/__init__.py ================================================ from __future__ import annotations from poetry.vcs.git.backend import Git __all__ = ["Git"] ================================================ FILE: src/poetry/vcs/git/backend.py ================================================ from __future__ import annotations import contextlib import dataclasses import logging import os import re from pathlib import Path from subprocess import CalledProcessError from typing import TYPE_CHECKING from urllib.parse import urljoin from urllib.parse import urlparse from urllib.parse import urlunparse from dulwich import porcelain from dulwich.client import HTTPUnauthorized from dulwich.client import get_transport_and_path from dulwich.config import ConfigFile from dulwich.config import parse_submodules from dulwich.errors import NotGitRepository from dulwich.file import FileLocked from dulwich.index import IndexEntry from dulwich.object_store import peel_sha from dulwich.objects import ObjectID from dulwich.protocol import PEELED_TAG_SUFFIX from dulwich.refs import Ref from dulwich.repo import Repo from poetry.console.exceptions import PoetryRuntimeError from poetry.utils.authenticator import get_default_authenticator from poetry.utils.helpers import remove_directory if TYPE_CHECKING: from dulwich.client import FetchPackResult from dulwich.client import GitClient logger = logging.getLogger(__name__) # A relative URL by definition starts with ../ or ./ RELATIVE_SUBMODULE_REGEX = re.compile(r"^\.{1,2}/") # Common error messages ERROR_MESSAGE_NOTE = ( "Note: This error arises from interacting with " "the specified vcs source and is likely not a " "Poetry issue." ) ERROR_MESSAGE_PROBLEMS_SECTION_START = ( "This issue could be caused by any of the following;\n" ) ERROR_MESSAGE_PROBLEMS_SECTION_START_NETWORK_ISSUES = ( f"{ERROR_MESSAGE_PROBLEMS_SECTION_START}\n" "- there are network issues in this environment" ) ERROR_MESSAGE_BAD_REVISION = ( "- the revision ({revision}) you have specified\n" " - was misspelled\n" " - is invalid (must be a sha or symref)\n" " - is not present on remote" ) ERROR_MESSAGE_BAD_REMOTE = ( "- the remote ({remote}) you have specified\n" " - was misspelled\n" " - does not exist\n" " - requires credentials that were either not configured or are incorrect\n" " - contains Git submodules that require credentials that were either not configured or are incorrect\n" " - is inaccessible due to network issues" ) ERROR_MESSAGE_FILE_LOCK = ( "- another process is holding the file lock\n" "- another process crashed while holding the file lock\n\n" "Try again later or remove the {lock_file} manually" " if you are sure no other process is holding it." ) def is_revision_sha(revision: str | None) -> bool: return re.match(r"^\b[0-9a-f]{5,40}\b$", revision or "") is not None def peeled_tag(ref: str | bytes) -> Ref: if isinstance(ref, str): ref = ref.encode("utf-8") return Ref(ref + PEELED_TAG_SUFFIX) @dataclasses.dataclass class GitRefSpec: branch: str | None = None revision: str | None = None tag: str | None = None ref: Ref = dataclasses.field(default_factory=lambda: Ref(b"HEAD")) def resolve(self, remote_refs: FetchPackResult, repo: Repo) -> None: """ Resolve the ref using the provided remote refs. """ self._normalise(remote_refs=remote_refs, repo=repo) self._set_head(remote_refs=remote_refs, repo=repo) def _normalise(self, remote_refs: FetchPackResult, repo: Repo) -> None: """ Internal helper method to determine if given revision is 1. a branch or tag; if so, set corresponding properties. 2. a short sha; if so, resolve full sha and set as revision """ if self.revision: ref = f"refs/tags/{self.revision}".encode() if ref in remote_refs.refs or peeled_tag(ref) in remote_refs.refs: # this is a tag, incorrectly specified as a revision, tags take priority self.tag = self.revision self.revision = None elif ( self.revision.encode("utf-8") in remote_refs.refs or f"refs/heads/{self.revision}".encode() in remote_refs.refs ): # this is most likely a ref spec or a branch incorrectly specified self.branch = self.revision self.revision = None elif ( self.branch and f"refs/heads/{self.branch}".encode() not in remote_refs.refs and ( f"refs/tags/{self.branch}".encode() in remote_refs.refs or peeled_tag(f"refs/tags/{self.branch}") in remote_refs.refs ) ): # this is a tag incorrectly specified as a branch self.tag = self.branch self.branch = None if self.revision and self.is_sha_short: # revision is a short sha, resolve to full sha short_sha = self.revision.encode("utf-8") for sha in remote_refs.refs.values(): if sha is not None and sha.startswith(short_sha): self.revision = sha.decode("utf-8") return # no heads with such SHA, let's check all objects for sha in repo.object_store.iter_prefix(short_sha): self.revision = sha.decode("utf-8") return def _set_head(self, remote_refs: FetchPackResult, repo: Repo) -> None: """ Internal helper method to populate ref and set it's sha as the remote's head and default ref. """ self.ref = remote_refs.symrefs[Ref(b"HEAD")] head: ObjectID | None if self.revision: head = ObjectID(self.revision.encode("utf-8")) else: if self.tag: ref = Ref(f"refs/tags/{self.tag}".encode()) peeled = peeled_tag(ref) self.ref = peeled if peeled in remote_refs.refs else ref elif self.branch: self.ref = ( Ref(self.branch.encode("utf-8")) if self.is_ref else Ref(f"refs/heads/{self.branch}".encode()) ) head = remote_refs.refs[self.ref] # Peel tag objects to get the underlying commit SHA. # Annotated tags are Tag objects, not Commit objects. Operations like # reset_index() expect HEAD to point to a Commit, so we must peel tags # to extract the commit SHA they reference. # Object not in store yet will be handled during fetch if head is not None: with contextlib.suppress(KeyError): head = peel_sha(repo.object_store, head)[1].id remote_refs.refs[self.ref] = remote_refs.refs[Ref(b"HEAD")] = head @property def key(self) -> str: return self.revision or self.branch or self.tag or self.ref.decode("utf-8") @property def is_sha(self) -> bool: return is_revision_sha(revision=self.revision) @property def is_ref(self) -> bool: return self.branch is not None and ( self.branch.startswith("refs/") or self.branch == "HEAD" ) @property def is_sha_short(self) -> bool: return self.revision is not None and self.is_sha and len(self.revision) < 40 @dataclasses.dataclass class GitRepoLocalInfo: repo: dataclasses.InitVar[Repo | Path] origin: str = dataclasses.field(init=False) revision: str = dataclasses.field(init=False) def __post_init__(self, repo: Repo | Path) -> None: repo = Git.as_repo(repo=repo) if not isinstance(repo, Repo) else repo self.origin = Git.get_remote_url(repo=repo, remote="origin") self.revision = Git.get_revision(repo=repo) class Git: @staticmethod def as_repo(repo: Path) -> Repo: return Repo(str(repo)) @staticmethod def get_remote_url(repo: Repo, remote: str = "origin") -> str: with repo: config = repo.get_config() section = (b"remote", remote.encode("utf-8")) url = "" if config.has_section(section): value = config.get(section, b"url") url = value.decode("utf-8") return url @staticmethod def get_revision(repo: Repo) -> str: with repo: return repo.get_peeled(Ref(b"HEAD")).decode("utf-8") @classmethod def info(cls, repo: Repo | Path) -> GitRepoLocalInfo: return GitRepoLocalInfo(repo=repo) @staticmethod def get_name_from_source_url(url: str) -> str: return re.sub(r"(.git)?$", "", url.rstrip("/").rsplit("/", 1)[-1]) @classmethod def _fetch_remote_refs(cls, url: str, local: Repo) -> FetchPackResult: """ Helper method to fetch remote refs. """ client: GitClient path: str credentials = get_default_authenticator().get_credentials_for_git_url(url=url) username = None password = None if credentials.password and credentials.username: # we do this conditionally as otherwise, dulwich might complain if these # parameters are passed in for an ssh url username = credentials.username password = credentials.password config = local.get_config_stack() client, path = get_transport_and_path( url, config=config, username=username, password=password ) with local: result: FetchPackResult = client.fetch( path, local, determine_wants=local.object_store.determine_wants_all, ) return result @staticmethod def _clone_legacy(url: str, refspec: GitRefSpec, target: Path) -> Repo: """ Helper method to facilitate fallback to using system provided git client via subprocess calls. """ from poetry.vcs.git.system import SystemGit logger.debug("Cloning '%s' using system git client", url) if target.exists(): remove_directory(path=target, force=True) revision = refspec.tag or refspec.branch or refspec.revision or "HEAD" try: SystemGit.clone(url, target) except CalledProcessError as e: raise PoetryRuntimeError.create( reason=f"Failed to clone {url}, check your git configuration and permissions for this repository.", exception=e, info=[ ERROR_MESSAGE_NOTE, ERROR_MESSAGE_PROBLEMS_SECTION_START_NETWORK_ISSUES, ERROR_MESSAGE_BAD_REMOTE.format(remote=url), ], ) if revision: revision.replace("refs/head/", "") revision.replace("refs/tags/", "") try: SystemGit.checkout(revision, target) except CalledProcessError as e: raise PoetryRuntimeError.create( reason=f"Failed to checkout {url} at '{revision}'.", exception=e, info=[ ERROR_MESSAGE_NOTE, ERROR_MESSAGE_PROBLEMS_SECTION_START_NETWORK_ISSUES, ERROR_MESSAGE_BAD_REVISION.format(revision=revision), ], ) repo = Repo(str(target)) return repo @classmethod def _clone(cls, url: str, refspec: GitRefSpec, target: Path) -> Repo: """ Helper method to clone a remove repository at the given `url` at the specified ref spec. """ local: Repo if not target.exists(): local = Repo.init(str(target), mkdir=True) porcelain.remote_add(local, "origin", url) else: local = Repo(str(target)) remote_refs = cls._fetch_remote_refs(url=url, local=local) logger.debug( "Cloning %s at '%s' to %s", url, refspec.key, target ) try: refspec.resolve(remote_refs=remote_refs, repo=local) except KeyError: # branch / ref does not exist raise PoetryRuntimeError.create( reason=f"Failed to clone {url} at '{refspec.key}', verify ref exists on remote.", info=[ ERROR_MESSAGE_NOTE, ERROR_MESSAGE_PROBLEMS_SECTION_START_NETWORK_ISSUES, ERROR_MESSAGE_BAD_REVISION.format(revision=refspec.key), ], ) try: # ensure local HEAD matches remote ref = remote_refs.refs[Ref(b"HEAD")] if ref is not None: local.refs[Ref(b"HEAD")] = ref except ValueError: raise PoetryRuntimeError.create( reason=f"Failed to clone {url} at '{refspec.key}', verify ref exists on remote.", info=[ ERROR_MESSAGE_NOTE, ERROR_MESSAGE_PROBLEMS_SECTION_START_NETWORK_ISSUES, ERROR_MESSAGE_BAD_REVISION.format(revision=refspec.key), f"\nThis particular error is prevalent when {refspec.key} could not be resolved to a specific commit sha.", ], ) if refspec.is_ref: # set ref to current HEAD local.refs[refspec.ref] = local.refs[Ref(b"HEAD")] for base, prefix in { (Ref(b"refs/remotes/origin"), b"refs/heads/"), (Ref(b"refs/tags"), b"refs/tags"), }: try: local.refs.import_refs( base=base, other={ Ref(n[len(prefix) :]): v for (n, v) in remote_refs.refs.items() if n.startswith(prefix) and not n.endswith(PEELED_TAG_SUFFIX) and v is not None }, ) except FileLocked as e: def to_str(path: bytes | str) -> str: if isinstance(path, bytes): path = path.decode() return path.replace(os.sep * 2, os.sep) raise PoetryRuntimeError.create( # should clean up the # ignore. reason=( f"Failed to clone {url} at '{refspec.key}'," f" unable to acquire file lock for {to_str(e.filename)}." ), info=[ ERROR_MESSAGE_NOTE, ERROR_MESSAGE_PROBLEMS_SECTION_START, ERROR_MESSAGE_FILE_LOCK.format( lock_file=to_str(e.lockfilename) ), ], ) try: with local: local.get_worktree().reset_index() except (AssertionError, KeyError) as e: # this implies the ref we need does not exist or is invalid if isinstance(e, KeyError): # the local copy is at a bad state, lets remove it logger.debug( "Removing local clone (%s) of repository as it is in a" " broken state.", local.path, ) remove_directory(Path(local.path), force=True) if isinstance(e, AssertionError) and "Invalid object name" not in str(e): raise raise PoetryRuntimeError.create( reason=f"Failed to clone {url} at '{refspec.key}', verify ref exists on remote.", info=[ ERROR_MESSAGE_NOTE, ERROR_MESSAGE_PROBLEMS_SECTION_START_NETWORK_ISSUES, ERROR_MESSAGE_BAD_REVISION.format(revision=refspec.key), ], exception=e, ) return local @classmethod def _clone_submodules(cls, repo: Repo) -> None: """ Helper method to identify configured submodules and clone them recursively. """ repo_root = Path(repo.path) for submodule in cls._get_submodules(repo): path_absolute = repo_root / submodule.path source_root = path_absolute.parent source_root.mkdir(parents=True, exist_ok=True) cls.clone( url=submodule.url, source_root=source_root, name=path_absolute.name, revision=submodule.revision, clean=path_absolute.exists() and not path_absolute.joinpath(".git").is_dir(), ) @classmethod def _get_submodules(cls, repo: Repo) -> list[SubmoduleInfo]: modules_config = Path(repo.path, ".gitmodules") if not modules_config.exists(): return [] config = ConfigFile.from_path(str(modules_config)) submodules: list[SubmoduleInfo] = [] for path, url, name in parse_submodules(config): url_str = url.decode("utf-8") path_str = path.decode("utf-8") name_str = name.decode("utf-8") if RELATIVE_SUBMODULE_REGEX.search(url_str): url_str = urlpathjoin(f"{cls.get_remote_url(repo)}/", url_str) with repo: index = repo.open_index() try: entry = index[path] except KeyError: logger.debug( "Skip submodule %s in %s, path %s not found", name, repo.path, path, ) continue assert isinstance(entry, IndexEntry) revision = entry.sha.decode("utf-8") submodules.append( SubmoduleInfo( path=path_str, url=url_str, name=name_str, revision=revision, ) ) return submodules @staticmethod def is_using_legacy_client() -> bool: from poetry.config.config import Config legacy_client: bool = Config.create().get("system-git-client", False) return legacy_client @staticmethod def get_default_source_root() -> Path: from poetry.config.config import Config return Path(Config.create().get("cache-dir")) / "src" @classmethod def clone( cls, url: str, name: str | None = None, branch: str | None = None, tag: str | None = None, revision: str | None = None, source_root: Path | None = None, clean: bool = False, ) -> Repo: source_root = source_root or cls.get_default_source_root() source_root.mkdir(parents=True, exist_ok=True) name = name or cls.get_name_from_source_url(url=url) target = source_root / name refspec = GitRefSpec(branch=branch, revision=revision, tag=tag) if target.exists(): if clean: # force clean the local copy if it exists, do not reuse remove_directory(target, force=True) else: # check if the current local copy matches the requested ref spec try: current_repo = Repo(str(target)) with current_repo: # we use peeled sha here to ensure tags are resolved consistently current_sha = current_repo.get_peeled(Ref(b"HEAD")).decode( "utf-8" ) except (NotGitRepository, AssertionError, KeyError): # something is wrong with the current checkout, clean it remove_directory(target, force=True) else: if not is_revision_sha(revision=current_sha): # head is not a sha, this will cause issues later, lets reset remove_directory(target, force=True) elif ( refspec.is_sha and refspec.revision is not None and current_sha.startswith(refspec.revision) ): # if revision is used short-circuit remote fetch head matches return current_repo try: if not cls.is_using_legacy_client(): local = cls._clone(url=url, refspec=refspec, target=target) cls._clone_submodules(repo=local) return local except HTTPUnauthorized: # we do this here to handle http authenticated repositories as dulwich # does not currently support using credentials from git-credential helpers. # upstream issue: https://github.com/jelmer/dulwich/issues/873 # # this is a little inefficient, however preferred as this is transparent # without additional configuration or changes for existing projects that # use http basic auth credentials. logger.debug( "Unable to fetch from private repository '%s', falling back to" " system git", url, ) # fallback to legacy git client return cls._clone_legacy(url=url, refspec=refspec, target=target) def urlpathjoin(base: str, path: str) -> str: """ Allow any URL to be joined with a path This works around an issue with urllib.parse.urljoin where it only handles relative URLs for protocols contained in urllib.parse.uses_relative. As it happens common protocols used with git, like ssh or git+ssh are not in that list. Thus we need to implement our own version of urljoin that handles all URLs protocols. This is accomplished by using urlparse and urlunparse to split the URL into its components, join the path, and then reassemble the URL. See: https://github.com/python-poetry/poetry/issues/6499#issuecomment-1564712609 """ parsed_base = urlparse(base) new = parsed_base._replace(path=urljoin(parsed_base.path, path)) return urlunparse(new) @dataclasses.dataclass class SubmoduleInfo: path: str url: str name: str revision: str ================================================ FILE: src/poetry/vcs/git/system.py ================================================ from __future__ import annotations import os import subprocess from typing import TYPE_CHECKING from dulwich.client import find_git_command if TYPE_CHECKING: from pathlib import Path from typing import Any class SystemGit: @classmethod def clone(cls, repository: str, dest: Path) -> None: cls._check_parameter(repository) cls.run("clone", "--recurse-submodules", "--", repository, str(dest)) @classmethod def checkout(cls, rev: str, target: Path | None = None) -> None: cls._check_parameter(rev) cls.run("checkout", rev, folder=target) @staticmethod def run(*args: Any, **kwargs: Any) -> None: folder = kwargs.pop("folder", None) if folder: args = ( "--git-dir", (folder / ".git").as_posix(), "--work-tree", folder.as_posix(), *args, ) git_command = find_git_command() env = os.environ.copy() env["GIT_TERMINAL_PROMPT"] = "0" subprocess.run( git_command + list(args), capture_output=True, env=env, text=True, encoding="utf-8", check=True, ) @staticmethod def _check_parameter(parameter: str) -> None: """ Checks a git parameter to avoid unwanted code execution. """ if parameter.strip().startswith("-"): raise RuntimeError(f"Invalid Git parameter: {parameter}") ================================================ FILE: src/poetry/version/__init__.py ================================================ ================================================ FILE: src/poetry/version/version_selector.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from poetry.core.packages.package import Package from poetry.repositories import RepositoryPool class VersionSelector: def __init__(self, pool: RepositoryPool) -> None: self._pool = pool def find_best_candidate( self, package_name: str, target_package_version: str | None = None, allow_prereleases: bool | None = None, source: str | None = None, ) -> Package | None: """ Given a package name and optional version, returns the latest Package that matches """ from poetry.factory import Factory dependency = Factory.create_dependency( package_name, { "version": target_package_version or "*", "allow-prereleases": allow_prereleases, "source": source, }, ) candidates = self._pool.find_packages(dependency) only_prereleases = all(c.version.is_unstable() for c in candidates) if not candidates: return None package = None for candidate in candidates: if ( candidate.is_prerelease() and not dependency.allows_prereleases() and not only_prereleases ): continue # Select highest version of the two if package is None or package.version < candidate.version: package = candidate return package ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/config/__init__.py ================================================ ================================================ FILE: tests/config/test_config.py ================================================ from __future__ import annotations import json import os import re from pathlib import Path from typing import TYPE_CHECKING from typing import Any import pytest from deepdiff.diff import DeepDiff from poetry.config.config import Config from poetry.config.config import boolean_normalizer from poetry.config.config import int_normalizer from poetry.utils.password_manager import PasswordManager from tests.helpers import flatten_dict from tests.helpers import isolated_environment if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterator from tests.conftest import DummyBackend Normalizer = Callable[[str], Any] def get_options_based_on_normalizer(normalizer: Normalizer) -> Iterator[str]: flattened_config = flatten_dict(obj=Config.default_config, delimiter=".") for k in flattened_config: if Config._get_normalizer(k) == normalizer: yield k @pytest.mark.parametrize( ("name", "value"), [ ("installer.parallel", True), ("virtualenvs.create", True), ("requests.max-retries", 0), ], ) def test_config_get_default_value(config: Config, name: str, value: bool) -> None: assert config.get(name) is value def test_config_get_processes_depended_on_values( config: Config, config_cache_dir: Path ) -> None: assert str(config_cache_dir / "virtualenvs") == config.get("virtualenvs.path") def generate_environment_variable_tests() -> Iterator[tuple[str, str, str, bool]]: data: list[tuple[Normalizer, list[tuple[str, Any]]]] = [ ( boolean_normalizer, [ ("true", True), ("false", False), ("True", True), ("False", False), ("1", True), ("0", False), ], ), (int_normalizer, [("4", 4), ("2", 2)]), ] for normalizer, values in data: for env_value, value in values: for name in get_options_based_on_normalizer(normalizer=normalizer): env_var = "POETRY_" + re.sub("[.-]+", "_", name).upper() yield name, env_var, env_value, value @pytest.mark.parametrize( ("name", "env_var", "env_value", "value"), list(generate_environment_variable_tests()), ) def test_config_get_from_environment_variable( config: Config, environ: Iterator[None], name: str, env_var: str, env_value: str, value: bool, ) -> None: os.environ[env_var] = env_value assert config.get(name) is value def test_config_get_from_environment_variable_nested( config: Config, environ: Iterator[None], ) -> None: options = config.default_config["virtualenvs"]["options"] expected = {} for k, v in options.items(): if isinstance(v, bool): expected[k] = not v os.environ[f"POETRY_VIRTUALENVS_OPTIONS_{k.upper().replace('-', '_')}"] = ( "true" if expected[k] else "false" ) assert config.get("virtualenvs.options") == expected @pytest.mark.parametrize( ("path_config", "expected"), [("~/.venvs", Path.home() / ".venvs"), ("venv", Path("venv"))], ) def test_config_expands_tilde_for_virtualenvs_path( config: Config, path_config: str, expected: Path ) -> None: config.merge({"virtualenvs": {"path": path_config}}) assert config.virtualenvs_path == expected def test_disabled_keyring_is_unavailable( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend ) -> None: manager = PasswordManager(config) assert manager.use_keyring config.config["keyring"]["enabled"] = False manager = PasswordManager(config) assert not manager.use_keyring @pytest.mark.parametrize( ("value", "invalid"), [ # non-serialised json ("BAD=VALUE", True), # good value (json.dumps({"CC": "gcc", "--build-option": ["--one", "--two"]}), False), # non-string key ('{0: "hello"}', True), # non-string value in list ('{"world": ["hello", 0]}', True), ], ) def test_config_get_from_environment_variable_build_config_settings( value: str, invalid: bool, config: Config, ) -> None: with isolated_environment( { "POETRY_INSTALLER_BUILD_CONFIG_SETTINGS_DEMO": value, }, clear=True, ): configured_value = config.get("installer.build-config-settings.demo") if invalid: assert configured_value is None else: assert not DeepDiff(configured_value, json.loads(value)) ================================================ FILE: tests/config/test_config_source.py ================================================ from __future__ import annotations from typing import Any import pytest from poetry.config.config_source import UNSET from poetry.config.config_source import ConfigSourceMigration from poetry.config.config_source import drop_empty_config_category from poetry.config.dict_config_source import DictConfigSource @pytest.mark.parametrize( ["config_data", "expected"], [ ( { "category_a": { "category_b": { "category_c": {}, }, }, "system-git-client": True, }, {"system-git-client": True}, ), ( { "category_a": { "category_b": { "category_c": {}, "category_d": {"some_config": True}, }, }, "system-git-client": True, }, { "category_a": { "category_b": { "category_d": {"some_config": True}, } }, "system-git-client": True, }, ), ], ) def test_drop_empty_config_category( config_data: dict[Any, Any], expected: dict[Any, Any] ) -> None: assert ( drop_empty_config_category( keys=["category_a", "category_b", "category_c"], config=config_data ) == expected ) def test_config_source_migration_rename_key() -> None: config_data = { "virtualenvs": { "prefer-active-python": True, }, "system-git-client": True, } config_source = DictConfigSource() config_source._config = config_data migration = ConfigSourceMigration( old_key="virtualenvs.prefer-active-python", new_key="virtualenvs.use-poetry-python", ) migration.apply(config_source) config_source._config = { "virtualenvs": { "use-poetry-python": True, }, "system-git-client": True, } def test_config_source_migration_remove_key() -> None: config_data = { "virtualenvs": { "prefer-active-python": True, }, "system-git-client": True, } config_source = DictConfigSource() config_source._config = config_data migration = ConfigSourceMigration( old_key="virtualenvs.prefer-active-python", new_key=None, ) migration.apply(config_source) config_source._config = { "virtualenvs": {}, "system-git-client": True, } def test_config_source_migration_unset_value() -> None: config_data = { "virtualenvs": { "prefer-active-python": True, }, "system-git-client": True, } config_source = DictConfigSource() config_source._config = config_data migration = ConfigSourceMigration( old_key="virtualenvs.prefer-active-python", new_key="virtualenvs.use-poetry-python", value_migration={True: UNSET, False: True}, ) migration.apply(config_source) config_source._config = { "virtualenvs": {}, "system-git-client": True, } def test_config_source_migration_complex_migration() -> None: config_data = { "virtualenvs": { "prefer-active-python": True, }, "system-git-client": True, } config_source = DictConfigSource() config_source._config = config_data migration = ConfigSourceMigration( old_key="virtualenvs.prefer-active-python", new_key="virtualenvs.use-poetry-python", value_migration={True: None, False: True}, ) migration.apply(config_source) config_source._config = { "virtualenvs": { "use-poetry-python": None, }, "system-git-client": True, } ================================================ FILE: tests/config/test_dict_config_source.py ================================================ from __future__ import annotations import pytest from poetry.config.config_source import PropertyNotFoundError from poetry.config.dict_config_source import DictConfigSource def test_dict_config_source_add_property() -> None: config_source = DictConfigSource() assert config_source._config == {} config_source.add_property("system-git-client", True) assert config_source._config == {"system-git-client": True} config_source.add_property("virtualenvs.use-poetry-python", False) assert config_source._config == { "virtualenvs": { "use-poetry-python": False, }, "system-git-client": True, } def test_dict_config_source_remove_property() -> None: config_data = { "virtualenvs": { "use-poetry-python": False, }, "system-git-client": True, } config_source = DictConfigSource() config_source._config = config_data config_source.remove_property("system-git-client") assert config_source._config == { "virtualenvs": { "use-poetry-python": False, } } config_source.remove_property("virtualenvs.use-poetry-python") assert config_source._config == {"virtualenvs": {}} def test_dict_config_source_get_property() -> None: config_data = { "virtualenvs": { "use-poetry-python": False, }, "system-git-client": True, } config_source = DictConfigSource() config_source._config = config_data assert config_source.get_property("virtualenvs.use-poetry-python") is False assert config_source.get_property("system-git-client") is True def test_dict_config_source_get_property_should_raise_if_not_found() -> None: config_source = DictConfigSource() with pytest.raises( PropertyNotFoundError, match=r"Key virtualenvs\.use-poetry-python not in config" ): _ = config_source.get_property("virtualenvs.use-poetry-python") ================================================ FILE: tests/config/test_file_config_source.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest import tomlkit from poetry.config.config_source import PropertyNotFoundError from poetry.config.file_config_source import FileConfigSource from poetry.toml import TOMLFile if TYPE_CHECKING: from pathlib import Path def test_file_config_source_add_property(tmp_path: Path) -> None: config = tmp_path.joinpath("config.toml") config.touch() config_source = FileConfigSource(TOMLFile(config)) assert config_source._file.read() == {} config_source.add_property("system-git-client", True) assert config_source._file.read() == {"system-git-client": True} config_source.add_property("virtualenvs.use-poetry-python", False) assert config_source._file.read() == { "virtualenvs": { "use-poetry-python": False, }, "system-git-client": True, } def test_file_config_source_remove_property(tmp_path: Path) -> None: config_data = { "virtualenvs": { "use-poetry-python": False, }, "system-git-client": True, } config = tmp_path.joinpath("config.toml") with config.open(mode="w", encoding="utf-8") as f: f.write(tomlkit.dumps(config_data)) config_source = FileConfigSource(TOMLFile(config)) config_source.remove_property("system-git-client") assert config_source._file.read() == { "virtualenvs": { "use-poetry-python": False, } } config_source.remove_property("virtualenvs.use-poetry-python") assert config_source._file.read() == {} def test_file_config_source_get_property(tmp_path: Path) -> None: config_data = { "virtualenvs": { "use-poetry-python": False, }, "system-git-client": True, } config = tmp_path.joinpath("config.toml") with config.open(mode="w", encoding="utf-8") as f: f.write(tomlkit.dumps(config_data)) config_source = FileConfigSource(TOMLFile(config)) assert config_source.get_property("virtualenvs.use-poetry-python") is False assert config_source.get_property("system-git-client") is True def test_file_config_source_get_property_should_raise_if_not_found( tmp_path: Path, ) -> None: config = tmp_path.joinpath("config.toml") config.touch() config_source = FileConfigSource(TOMLFile(config)) with pytest.raises( PropertyNotFoundError, match=r"Key virtualenvs\.use-poetry-python not in config" ): _ = config_source.get_property("virtualenvs.use-poetry-python") ================================================ FILE: tests/config/test_source.py ================================================ from __future__ import annotations import pytest from tomlkit.container import Container from tomlkit.items import Table from tomlkit.items import Trivia from poetry.config.source import Source from poetry.repositories.repository_pool import Priority @pytest.mark.parametrize( "source,table_body", [ ( Source("foo", "https://example.com"), { "name": "foo", "priority": "primary", "url": "https://example.com", }, ), ( Source("bar", "https://example.com/bar", priority=Priority.EXPLICIT), { "name": "bar", "priority": "explicit", "url": "https://example.com/bar", }, ), ], ) def test_source_to_table(source: Source, table_body: dict[str, str | bool]) -> None: table = Table(Container(), Trivia(), False) table._value = table_body # type: ignore[assignment] assert source.to_toml_table() == table def test_source_default_is_primary() -> None: source = Source("foo", "https://example.com") assert source.priority == Priority.PRIMARY @pytest.mark.parametrize( ("priority", "expected_priority"), [ ("supplemental", Priority.SUPPLEMENTAL), ("SUPPLEMENTAL", Priority.SUPPLEMENTAL), ], ) def test_source_priority_as_string(priority: str, expected_priority: Priority) -> None: source = Source( "foo", "https://example.com", priority=priority, # type: ignore[arg-type] ) assert source.priority == Priority.SUPPLEMENTAL ================================================ FILE: tests/conftest.py ================================================ from __future__ import annotations import contextlib import logging import os import platform import re import shutil import sys from collections.abc import Iterator from pathlib import Path from typing import TYPE_CHECKING import findpython import keyring import packaging.version import pytest import responses from installer.utils import SCHEME_NAMES from jaraco.classes import properties from keyring.backend import KeyringBackend from keyring.backends.fail import Keyring as FailKeyring from keyring.credentials import SimpleCredential from keyring.errors import KeyringError from keyring.errors import KeyringLocked from packaging.utils import canonicalize_name from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency import Dependency from poetry.core.utils._compat import WINDOWS from poetry.core.version.markers import parse_marker from pytest import FixtureRequest from poetry.config.config import Config as BaseConfig from poetry.config.dict_config_source import DictConfigSource from poetry.console.commands.command import Command from poetry.factory import Factory from poetry.layouts import layout from poetry.packages.direct_origin import _get_package_from_git from poetry.repositories import Repository from poetry.repositories import RepositoryPool from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.installed_repository import InstalledRepository from poetry.utils.cache import ArtifactCache from poetry.utils.env import EnvManager from poetry.utils.env import MockEnv from poetry.utils.env import SystemEnv from poetry.utils.env import VirtualEnv from poetry.utils.env.python import Python from poetry.utils.password_manager import PoetryKeyring from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import TestLocker from tests.helpers import TestRepository from tests.helpers import get_package from tests.helpers import http_setup_redirect from tests.helpers import isolated_environment from tests.helpers import mock_clone from tests.helpers import set_keyring_backend from tests.helpers import switch_working_directory from tests.helpers import with_working_directory if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterator from collections.abc import Mapping from typing import Any from unittest.mock import MagicMock from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from keyring.credentials import Credential from packaging.utils import NormalizedName from poetry.core.packages.package import Package from pytest import Config as PyTestConfig from pytest import Parser from pytest import TempPathFactory from pytest_mock import MockerFixture from poetry.poetry import Poetry from poetry.utils.env.base_env import PythonVersion from tests.types import CommandFactory from tests.types import FixtureCopier from tests.types import FixtureDirGetter from tests.types import MockedPoetryPythonRegister from tests.types import MockedPythonRegister from tests.types import PackageFactory from tests.types import ProjectFactory from tests.types import SetProjectContext pytest_plugins = [ "tests.repositories.fixtures", ] def pytest_addoption(parser: Parser) -> None: parser.addoption( "--integration", action="store_true", dest="integration", default=False, help="enable integration tests", ) def pytest_configure(config: PyTestConfig) -> None: config.addinivalue_line("markers", "integration: mark integration tests") if not config.option.integration: if config.option.markexpr: config.option.markexpr += " and not integration" else: config.option.markexpr = "not integration" class Config(BaseConfig): _config_source: DictConfigSource _auth_config_source: DictConfigSource def get(self, setting_name: str, default: Any = None) -> Any: self.merge(self._config_source.config) self.merge(self._auth_config_source.config) return super().get(setting_name, default=default) def raw(self) -> dict[str, Any]: self.merge(self._config_source.config) self.merge(self._auth_config_source.config) return super().raw() def all(self) -> dict[str, Any]: self.merge(self._config_source.config) self.merge(self._auth_config_source.config) return super().all() class DummyBackend(KeyringBackend): def __init__(self) -> None: self._passwords: dict[str, dict[str, str]] = {} self._service_defaults: dict[str, Credential] = {} @properties.classproperty def priority(self) -> float: return 42 def set_password(self, service: str, username: str, password: str) -> None: self._passwords[service] = {username: password} def get_password(self, service: str, username: str) -> str | None: return self._passwords.get(service, {}).get(username) def get_credential( self, service: str, username: str | None, ) -> Credential | None: if username is None: credential = self._service_defaults.get(service) return credential password = self.get_password(service, username) if password is None: return None return SimpleCredential(username, password) def delete_password(self, service: str, username: str) -> None: if service in self._passwords and username in self._passwords[service]: del self._passwords[service][username] def set_default_service_credential( self, service: str, credential: Credential ) -> None: self._service_defaults[service] = credential class LockedBackend(KeyringBackend): @properties.classproperty def priority(self) -> float: return 42 def set_password(self, service: str, username: str, password: str) -> None: raise KeyringLocked() def get_password(self, service: str, username: str) -> str | None: raise KeyringLocked() def get_credential( self, service: str, username: str | None, ) -> Credential | None: raise KeyringLocked() def delete_password(self, service: str, username: str) -> None: raise KeyringLocked() class ErroneousBackend(FailKeyring): @properties.classproperty def priority(self) -> float: return 42 def get_credential( self, service: str, username: str | None, ) -> Credential | None: raise KeyringError() @pytest.fixture() def poetry_keyring() -> PoetryKeyring: return PoetryKeyring("poetry-repository") @pytest.fixture() def dummy_keyring() -> DummyBackend: return DummyBackend() @pytest.fixture() def with_simple_keyring(dummy_keyring: DummyBackend) -> None: set_keyring_backend(dummy_keyring) @pytest.fixture() def with_fail_keyring() -> None: set_keyring_backend(FailKeyring()) # type: ignore[no-untyped-call] @pytest.fixture() def with_locked_keyring() -> None: set_keyring_backend(LockedBackend()) # type: ignore[no-untyped-call] @pytest.fixture() def with_erroneous_keyring() -> None: set_keyring_backend(ErroneousBackend()) # type: ignore[no-untyped-call] @pytest.fixture() def with_null_keyring() -> None: from keyring.backends.null import Keyring set_keyring_backend(Keyring()) # type: ignore[no-untyped-call] @pytest.fixture() def with_chained_fail_keyring(mocker: MockerFixture) -> None: mocker.patch( "keyring.backend.get_all_keyring", lambda: [FailKeyring()], # type: ignore[no-untyped-call] ) from keyring.backends.chainer import ChainerBackend set_keyring_backend(ChainerBackend()) # type: ignore[no-untyped-call] @pytest.fixture() def with_chained_null_keyring(mocker: MockerFixture) -> None: from keyring.backends.null import Keyring mocker.patch( "keyring.backend.get_all_keyring", lambda: [Keyring()], # type: ignore[no-untyped-call] ) from keyring.backends.chainer import ChainerBackend set_keyring_backend(ChainerBackend()) # type: ignore[no-untyped-call] @pytest.fixture def config_cache_dir(tmp_path: Path) -> Path: path = tmp_path / ".cache" / "pypoetry" path.mkdir(parents=True) return path @pytest.fixture def config_data_dir(tmp_path: Path) -> Path: path = tmp_path / ".local" / "share" / "pypoetry" path.mkdir(parents=True) return path @pytest.fixture def config_virtualenvs_path(config_cache_dir: Path) -> Path: return config_cache_dir / "virtualenvs" @pytest.fixture def config_source(config_cache_dir: Path, config_data_dir: Path) -> DictConfigSource: source = DictConfigSource() source.add_property("cache-dir", str(config_cache_dir)) source.add_property("data-dir", str(config_data_dir)) return source @pytest.fixture def auth_config_source() -> DictConfigSource: source = DictConfigSource() return source @pytest.fixture(autouse=True) def config( config_source: DictConfigSource, auth_config_source: DictConfigSource, mocker: MockerFixture, ) -> Config: keyring.set_keyring(FailKeyring()) # type: ignore[no-untyped-call] c = Config() c.merge(config_source.config) c.set_config_source(config_source) c.set_auth_config_source(auth_config_source) mocker.patch("poetry.config.config.Config.create", return_value=c) mocker.patch("poetry.config.config.Config.set_config_source") return c @pytest.fixture def artifact_cache(config: Config) -> ArtifactCache: return ArtifactCache(cache_dir=config.artifacts_cache_directory) @pytest.fixture() def config_dir(tmp_path: Path) -> Path: path = tmp_path / "config" path.mkdir() return path @pytest.fixture(autouse=True) def mock_user_config_dir(mocker: MockerFixture, config_dir: Path) -> None: mocker.patch("poetry.locations.CONFIG_DIR", new=config_dir) mocker.patch("poetry.config.config.CONFIG_DIR", new=config_dir) @pytest.fixture def environ() -> Iterator[None]: with isolated_environment(): yield @pytest.fixture(autouse=True) def isolate_environ() -> Iterator[None]: """Ensure the environment is isolated from user configuration.""" with isolated_environment(): for var in os.environ: if var.startswith("POETRY_") or var in {"PYTHONPATH", "VIRTUAL_ENV"}: del os.environ[var] yield @pytest.fixture(autouse=True) def git_mock(mocker: MockerFixture, request: FixtureRequest) -> None: if request.node.get_closest_marker("skip_git_mock"): return # Patch git module to not actually clone projects mocker.patch("poetry.vcs.git.Git.clone", new=mock_clone) p = mocker.patch("poetry.vcs.git.Git.get_revision") p.return_value = MOCK_DEFAULT_GIT_REVISION _get_package_from_git.cache_clear() @pytest.fixture def http() -> Iterator[responses.RequestsMock]: with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: yield rsps @pytest.fixture def http_redirector(http: responses.RequestsMock) -> None: http_setup_redirect( http, responses.HEAD, responses.GET, responses.PUT, responses.POST ) @pytest.fixture def project_root() -> Path: return Path(__file__).parent.parent @pytest.fixture(scope="session") def fixture_base() -> Path: return Path(__file__).parent / "fixtures" @pytest.fixture(scope="session") def fixture_dir(fixture_base: Path) -> FixtureDirGetter: def _fixture_dir(name: str) -> Path: return fixture_base / name return _fixture_dir @pytest.fixture def tmp_venv(tmp_path: Path) -> Iterator[VirtualEnv]: venv_path = tmp_path / "venv" EnvManager.build_venv(venv_path) venv = VirtualEnv(venv_path) yield venv shutil.rmtree(venv.path) @pytest.fixture def installed() -> InstalledRepository: return InstalledRepository() @pytest.fixture(scope="session") def current_env() -> SystemEnv: return SystemEnv(Path(sys.executable)) @pytest.fixture(scope="session") def current_python(current_env: SystemEnv) -> PythonVersion: return current_env.version_info @pytest.fixture(scope="session") def default_python(current_python: PythonVersion) -> str: return "^" + ".".join(str(v) for v in current_python[:2]) @pytest.fixture def repo(http: responses.RequestsMock) -> TestRepository: http.get(re.compile(r"^https?://foo\.bar/(.+?)$")) return TestRepository(name="foo") @pytest.fixture def project_factory( tmp_path: Path, config: Config, repo: TestRepository, installed: InstalledRepository, default_python: str, load_required_fixtures: None, ) -> ProjectFactory: workspace = tmp_path def _factory( name: str | None = None, dependencies: Mapping[str, str] | None = None, dev_dependencies: Mapping[str, str] | None = None, pyproject_content: str | None = None, poetry_lock_content: str | None = None, install_deps: bool = True, source: Path | None = None, locker_config: dict[str, Any] | None = None, use_test_locker: bool = True, ) -> Poetry: project_dir = workspace / f"poetry-fixture-{name}" dependencies = dependencies or {} dev_dependencies = dev_dependencies or {} if pyproject_content or source: if source: project_dir.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(source, project_dir) else: project_dir.mkdir(parents=True, exist_ok=True) if pyproject_content: with (project_dir / "pyproject.toml").open("w", encoding="utf-8") as f: f.write(pyproject_content) else: assert name is not None layout("src")( name, "0.1.0", author="PyTest Tester ", readme_format="md", python=default_python, dependencies=dependencies, dev_dependencies=dev_dependencies, ).create(project_dir, with_tests=False) if poetry_lock_content: lock_file = project_dir / "poetry.lock" lock_file.write_text(data=poetry_lock_content, encoding="utf-8") poetry = Factory().create_poetry(project_dir) if use_test_locker: locker = TestLocker( poetry.locker.lock, locker_config or poetry.locker._pyproject_data ) locker.write() poetry.set_locker(locker) poetry.set_config(config) pool = RepositoryPool() pool.add_repository(repo) poetry.set_pool(pool) if install_deps: for deps in [dependencies, dev_dependencies]: for name, version in deps.items(): pkg = get_package(name, version) repo.add_package(pkg) installed.add_package(pkg) return poetry return _factory @pytest.fixture def create_package(repo: Repository) -> PackageFactory: """ This function is a pytest fixture that creates a factory function to generate and customize package objects. These packages are added to the default repository fixture and configured with specific versions, optional extras, and self-referenced extras. This helps in setting up package dependencies for testing purposes. :return: A factory function that can be used to create and configure packages. """ def create_new_package( name: str, version: str | None = None, dependencies: list[Dependency] | None = None, extras: dict[str, list[str]] | None = None, merge_extras: bool = False, ) -> Package: version = version or "1.0" package = get_package(name, version) package_extras: dict[NormalizedName, list[Dependency]] = {} for extra, extra_dependencies in (extras or {}).items(): extra = canonicalize_name(extra) if extra not in package_extras: package_extras[extra] = [] for extra_dependency_spec in extra_dependencies: extra_dependency = Dependency.create_from_pep_508(extra_dependency_spec) extra_dependency._optional = True extra_dependency.marker = extra_dependency.marker.intersect( parse_marker(f"extra == '{extra}'") ) if extra_dependency.name != package.name: assert extra_dependency.constraint.allows(package.version) # if it is not a self-referencing dependency, make sure we add it to the repo try: pkg = repo.package(extra_dependency.name, package.version) except PackageNotFoundError: pkg = get_package(extra_dependency.name, str(package.version)) repo.add_package(pkg) extra_dependency.constraint = parse_constraint(f"^{pkg.version}") if merge_extras: # if requirement already exists in the package, # update the marker for requirement in package.requires: if ( requirement.name == extra_dependency.name and requirement.is_optional() ): requirement.marker = requirement.marker.union( extra_dependency.marker ) break else: package.add_dependency(extra_dependency) else: package.add_dependency(extra_dependency) package_extras[extra].append(extra_dependency) package.extras = package_extras for dependency in dependencies or []: package.add_dependency(dependency) repo.add_package(package) return package return create_new_package @pytest.fixture(autouse=True) def set_simple_log_formatter() -> None: """ This fixture removes any formatting added via IOFormatter. """ for name in logging.Logger.manager.loggerDict: for handler in logging.getLogger(name).handlers: # replace formatter with simple formatter for testing handler.setFormatter(logging.Formatter(fmt="%(message)s")) @pytest.fixture def fixture_copier(fixture_base: Path, tmp_path: Path) -> FixtureCopier: def _copy(relative_path: str, target: Path | None = None) -> Path: path = fixture_base / relative_path target = target or (tmp_path / relative_path) target.parent.mkdir(parents=True, exist_ok=True) if target.exists(): return target if path.is_dir(): shutil.copytree(path, target) else: shutil.copyfile(path, target) return target return _copy @pytest.fixture def required_fixtures() -> list[str]: return [] @pytest.fixture(autouse=True) def load_required_fixtures( required_fixtures: list[str], fixture_copier: FixtureCopier ) -> None: for fixture in required_fixtures: fixture_copier(fixture) @pytest.fixture def venv_flags_default() -> dict[str, bool]: return { "always-copy": False, "system-site-packages": False, "no-pip": False, } @pytest.fixture def disable_http_status_force_list(mocker: MockerFixture) -> Iterator[None]: mocker.patch("poetry.utils.authenticator.STATUS_FORCELIST", []) yield @pytest.fixture(autouse=True) def tmp_working_directory(tmp_path: Path) -> Iterator[Path]: with switch_working_directory(tmp_path): yield tmp_path @pytest.fixture(autouse=True, scope="session") def tmp_session_working_directory(tmp_path_factory: TempPathFactory) -> Iterator[Path]: tmp_path = tmp_path_factory.mktemp("session-working-directory") with switch_working_directory(tmp_path): yield tmp_path @pytest.fixture def set_project_context( tmp_working_directory: Path, tmp_path: Path, fixture_dir: FixtureDirGetter ) -> SetProjectContext: @contextlib.contextmanager def project_context(project: str | Path, in_place: bool = False) -> Iterator[Path]: if isinstance(project, str): project = fixture_dir(project) with with_working_directory( source=project, target=tmp_path.joinpath(project.name) if not in_place else None, ) as path: yield path return project_context @pytest.fixture def command_factory() -> CommandFactory: """ Provides a pytest fixture for creating mock commands using a factory function. This fixture allows for customization of command attributes like name, arguments, options, description, help text, and handler. """ def _command_factory( command_name: str, command_arguments: list[Argument] | None = None, command_options: list[Option] | None = None, command_description: str = "", command_help: str = "", command_handler: Callable[[Command], int] | str | None = None, ) -> Command: class MockCommand(Command): name = command_name arguments = command_arguments or [] options = command_options or [] description = command_description help = command_help def handle(self) -> int: if command_handler is not None and not isinstance(command_handler, str): return command_handler(self) self._io.write_line( command_handler or f"The mock command '{command_name}' has been called" ) return 0 return MockCommand() return _command_factory @pytest.fixture(autouse=True) def default_keyring(with_null_keyring: None) -> None: pass @pytest.fixture def system_env(tmp_path_factory: TempPathFactory, mocker: MockerFixture) -> SystemEnv: base_path = tmp_path_factory.mktemp("system_env") env = MockEnv(path=base_path, sys_path=[str(base_path / "purelib")]) assert env.path.is_dir() userbase = env.path / "userbase" userbase.mkdir(exist_ok=False) env.paths["userbase"] = str(userbase) paths = {str(scheme): str(env.path / scheme) for scheme in SCHEME_NAMES} env.paths.update(paths) for path in paths.values(): Path(path).mkdir(exist_ok=False) mocker.patch.object(EnvManager, "get_system_env", return_value=env) env.set_paths() return env @pytest.fixture def mocked_pythons() -> list[findpython.PythonVersion]: """ Fixture that provides a mock representation of Python versions that are registered. This fixture returns a list of `findpython.PythonVersion` objects. Typically, it is used in test scenarios to replace actual Python version discovery with mocked data. By default, this fixture returns an empty list to simulate an environment without any Python installations. :return: Mocked list of Python versions with the type of `findpython.PythonVersion`. """ return [] @pytest.fixture def mocked_pythons_version_map() -> dict[str, findpython.PythonVersion]: """ Create a mocked Python version map for testing purposes. This serves as a quick lookup for exact version matches. This function provides a fixture that returns a dictionary containing a mapping of specific keys to corresponding instances of the `findpython.PythonVersion` class. This is primarily used for testing scenarios involving multiple Python interpreters. If the key is an empty string, it maps to the system Python interpreter as used by the `with_mocked_findpython` fixture. :return: A dictionary mapping string keys to `findpython.PythonVersion` instances. A default key "" (empty string) is pre-set to match the current system environment. """ return { # add the system python if key is empty "": Python.get_system_python()._python } @pytest.fixture def mock_findpython_find( mocked_pythons: list[findpython.PythonVersion], mocked_pythons_version_map: dict[str, findpython.PythonVersion], mocker: MockerFixture, ) -> MagicMock: """ Mock the `findpython.find` function for testing purposes, enabling controlled execution and predictable results when specific python versions or executables are queried. This mock is particularly useful for reproducing various scenarios involving Python version detection without dependence on the actual system's Python installations. :return: A `MagicMock` object representing the mocked `findpython.find` function. It operates using the `_find` internal function, which resolves python versions based on the provided test data (`mocked_pythons` and `mocked_pythons_version_map`). """ def _find( name: str | None = None, ) -> findpython.PythonVersion | None: # find exact version matches # the default key is an empty string in mocked_pythons_version_map if python := mocked_pythons_version_map.get(name or ""): return python if name is None: return None candidates: list[findpython.PythonVersion] = [] # iterate through to find executable name match for python in mocked_pythons: if python.executable.name == name: return python elif str(python.executable).endswith(name): candidates.append(python) if candidates: candidates.sort(key=lambda p: p.executable.name) return candidates[0] return None return mocker.patch( "findpython.find", side_effect=_find, ) @pytest.fixture def mock_findpython_find_all( mocked_pythons: list[findpython.PythonVersion], mocker: MockerFixture, ) -> MagicMock: """ Mocks the `find_all` function in the `findpython` module to return a predefined list of `PythonVersion` objects. This fixture is useful for testing functionality dependent on the output of the `find_all` function without executing its original logic. :return: Mocked `find_all` function patched to return the specified list of `mocked_pythons`. """ return mocker.patch( "findpython.find_all", return_value=mocked_pythons, ) @pytest.fixture def mocked_python_register( with_mocked_findpython: None, mocked_pythons: list[findpython.PythonVersion], mocked_pythons_version_map: dict[str, findpython.PythonVersion], mocker: MockerFixture, ) -> MockedPythonRegister: """ Fixture to provide a mocked registration mechanism for PythonVersion objects. The fixture interacts with mocked versions of Python, allowing test cases to register and manage Python versions under controlled conditions. The provided register function enables the dynamic registration of Python versions, executable, and optional system designation. :return: A function to register a Python version with configurable options. """ def register( version: str, executable_name: str | Path | None = None, implementation: str | None = None, free_threaded: bool = False, parent: str | Path | None = None, make_system: bool = False, ) -> Python: # we allow this to let windows specific tests setup special cases parent = Path(parent or "/usr/bin") if not executable_name: info = version.split(".") executable_name = f"python{info[0]}.{info[1]}" class MockPythonVersion(findpython.PythonVersion): # type: ignore[misc] @property def implementation(self) -> str: return implementation or platform.python_implementation() @property def freethreaded(self) -> bool: return free_threaded python = MockPythonVersion( executable=parent / executable_name, _version=packaging.version.Version(version), _interpreter=parent / executable_name, ) mocked_pythons.append(python) mocked_pythons_version_map[version] = python if make_system: mocker.patch( "poetry.utils.env.python.Python.get_system_python", return_value=Python(python=python), ) mocked_pythons_version_map[""] = python return Python(python=python) return register @pytest.fixture def without_mocked_findpython( mock_findpython_find: MagicMock, mock_findpython_find_all: MagicMock, mocker: MockerFixture, ) -> None: """ This fixture stops the mocks for the functions `mock_findpython_find_all` and `mock_findpython_find`. It is intended for use within unit tests to ensure that the actual behavior of the mocked functions is not included unless explicitly required. """ mocker.stop(mock_findpython_find_all) mocker.stop(mock_findpython_find) @pytest.fixture(autouse=True) def with_mocked_findpython( mock_findpython_find: MagicMock, mock_findpython_find_all: MagicMock, ) -> None: """ Fixture that mocks the `findpython` library functions `find` and `find_all`. This fixture enables controlled testing of Python version discovery by providing mocked data for `findpython.PythonVersion` objects and behavior. It patches the `findpython.find` and `findpython.find_all` methods using the given mock data to simulate real functionality. This function mock behavior includes: - Finding Python versions by an exact match of executable name or selectable from candidates whose executable names end with the provided input. - Returning all mocked Python versions through the `findpython.find_all`. See also the `without_mocked_findpython`, `mocked_python_register`, `mock_findpython_find`, and `mock_findpython_find_all` fixtures. """ return @pytest.fixture def with_no_active_python(mocker: MockerFixture) -> MagicMock: return mocker.patch( "poetry.utils.env.python.Python.get_active_python", return_value=None, ) @pytest.fixture def mock_python_version(mocker: MockerFixture) -> None: class MockPythonVersion(findpython.PythonVersion): # type: ignore[misc] @property def implementation(self) -> str: return "PyPy" if "pypy" in str(self.executable) else "CPython" @property def freethreaded(self) -> bool: return self._install_dir.name.endswith("t") @property def _install_dir(self) -> Path: install_dir = self.executable.parent if not WINDOWS: install_dir = install_dir.parent assert isinstance(install_dir, Path) return install_dir def _get_version(self) -> packaging.version.Version: return packaging.version.Version( self._install_dir.name.removesuffix("t").split("@")[1] ) def _get_architecture(self) -> str: return "64bit" def _get_interpreter(self) -> str: return str(self.executable) mocker.patch( "poetry.utils.env.python.providers.PoetryPythonPathProvider.version_maker", MockPythonVersion, ) @pytest.fixture def mocked_poetry_managed_python_register( config: Config, without_mocked_findpython: None, mock_python_version: None ) -> MockedPoetryPythonRegister: config.python_installation_dir.mkdir() def register( version: str, implementation: str, free_threaded: bool = False, with_install_dir: bool = False, ) -> Path: python_dir_name = f"{implementation}@{version}" if free_threaded: python_dir_name += "t" bin_dir = config.python_installation_dir / python_dir_name if with_install_dir: bin_dir /= "install" if not WINDOWS: bin_dir /= "bin" bin_dir.mkdir(parents=True) (bin_dir / "python").touch() if implementation == "pypy": (bin_dir / "pypy").touch() return bin_dir return register ================================================ FILE: tests/console/__init__.py ================================================ ================================================ FILE: tests/console/commands/__init__.py ================================================ ================================================ FILE: tests/console/commands/cache/__init__.py ================================================ ================================================ FILE: tests/console/commands/cache/conftest.py ================================================ from __future__ import annotations import uuid from typing import TYPE_CHECKING import pytest from poetry.utils.cache import FileCache if TYPE_CHECKING: from pathlib import Path from tests.conftest import Config @pytest.fixture def repository_cache_dir(config: Config) -> Path: return config.repository_cache_directory @pytest.fixture def repositories() -> list[str]: return [f"01_{uuid.uuid4()}", f"02_{uuid.uuid4()}"] @pytest.fixture def repository_dirs( repository_cache_dir: Path, repositories: list[str], ) -> list[Path]: return [ repository_cache_dir / repositories[0], repository_cache_dir / repositories[1], ] @pytest.fixture def caches( repository_dirs: list[Path], ) -> list[FileCache[dict[str, str]]]: repository_dirs[0].mkdir(parents=True) repository_dirs[1].mkdir(parents=True) caches: list[FileCache[dict[str, str]]] = [ FileCache(path=repository_dirs[0]), FileCache(path=repository_dirs[1]), ] caches[0].remember( "cachy:0.1", lambda: {"name": "cachy", "version": "0.1"}, minutes=None ) caches[0].remember( "cleo:0.2", lambda: {"name": "cleo", "version": "0.2"}, minutes=None ) caches[1].remember( "cachy:0.1", lambda: {"name": "cachy", "version": "0.1"}, minutes=None ) # different version of same package, other entry than in first cache caches[1].remember( "cashy:0.2", lambda: {"name": "cashy", "version": "0.2"}, minutes=None ) return caches ================================================ FILE: tests/console/commands/cache/test_clear.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import TypeVar import pytest from cleo.testers.application_tester import ApplicationTester from poetry.console.application import Application if TYPE_CHECKING: from pathlib import Path from poetry.utils.cache import FileCache T = TypeVar("T") @pytest.fixture def tester() -> ApplicationTester: app = Application() tester = ApplicationTester(app) return tester @pytest.mark.parametrize("inputs", ["yes", "no"]) def test_cache_clear_all( tester: ApplicationTester, repository_cache_dir: Path, repositories: list[str], repository_dirs: list[Path], caches: list[FileCache[dict[str, str]]], inputs: str, ) -> None: exit_code = tester.execute("cache clear --all", inputs=inputs) assert exit_code == 0 assert tester.io.fetch_output() == "" if inputs == "yes": assert not repository_dirs[0].exists() or not any(repository_dirs[0].iterdir()) assert not repository_dirs[1].exists() or not any(repository_dirs[1].iterdir()) assert not caches[0].has("cachy:0.1") assert not caches[0].has("cleo:0.2") assert not caches[1].has("cachy:0.1") assert not caches[1].has("cashy:0.2") else: assert any((repository_cache_dir / repositories[0]).iterdir()) assert any((repository_cache_dir / repositories[1]).iterdir()) assert caches[0].has("cachy:0.1") assert caches[0].has("cleo:0.2") assert caches[1].has("cachy:0.1") assert caches[1].has("cashy:0.2") @pytest.mark.parametrize("inputs", ["yes", "no"]) def test_cache_clear_all_one_cache( tester: ApplicationTester, repository_cache_dir: Path, repositories: list[str], repository_dirs: list[Path], caches: list[FileCache[dict[str, str]]], inputs: str, ) -> None: exit_code = tester.execute(f"cache clear {repositories[0]} --all", inputs=inputs) assert exit_code == 0 assert tester.io.fetch_output() == "" if inputs == "yes": assert not repository_dirs[0].exists() or not any(repository_dirs[0].iterdir()) assert not caches[0].has("cachy:0.1") assert not caches[0].has("cleo:0.2") else: assert any((repository_cache_dir / repositories[0]).iterdir()) assert caches[0].has("cachy:0.1") assert caches[0].has("cleo:0.2") assert any((repository_cache_dir / repositories[1]).iterdir()) assert caches[1].has("cachy:0.1") assert caches[1].has("cashy:0.2") def test_cache_clear_all_no_entries(tester: ApplicationTester) -> None: exit_code = tester.execute("cache clear --all") assert exit_code == 0 assert tester.io.fetch_output().strip() == "No cache entries" def test_cache_clear_all_one_cache_no_entries( tester: ApplicationTester, repository_cache_dir: Path, repositories: list[str], ) -> None: exit_code = tester.execute(f"cache clear {repositories[0]} --all") assert exit_code == 0 assert tester.io.fetch_output().strip() == f"No cache entries for {repositories[0]}" @pytest.mark.parametrize("with_repo", [False, True]) def test_cache_clear_missing_option( tester: ApplicationTester, repositories: list[str], with_repo: bool ) -> None: command = f"cache clear {repositories[0]}" if with_repo else "cache clear" exit_code = tester.execute(command) assert exit_code == 1 assert ( "Add the --all option if you want to clear all cache entries" in tester.io.fetch_error() ) @pytest.mark.parametrize("inputs", ["yes", "no"]) @pytest.mark.parametrize("package_name", ["cachy", "Cachy"]) def test_cache_clear_pkg( tester: ApplicationTester, repositories: list[str], caches: list[FileCache[dict[str, str]]], package_name: str, inputs: str, ) -> None: exit_code = tester.execute( f"cache clear {repositories[1]}:{package_name}:0.1", inputs=inputs ) assert exit_code == 0 assert tester.io.fetch_output() == "" if inputs == "yes": assert not caches[1].has("cachy:0.1") assert caches[1].has("cashy:0.2") else: assert caches[1].has("cachy:0.1") assert caches[1].has("cashy:0.2") assert caches[0].has("cachy:0.1") ================================================ FILE: tests/console/commands/cache/test_list.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from pathlib import Path from cleo.testers.command_tester import CommandTester from poetry.utils.cache import FileCache from tests.types import CommandTesterFactory @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("cache list") def test_cache_list( tester: CommandTester, caches: list[FileCache[dict[str, str]]], repositories: list[str], ) -> None: tester.execute() expected = f"""\ {repositories[0]} {repositories[1]} """ assert tester.io.fetch_output() == expected def test_cache_list_empty(tester: CommandTester, repository_cache_dir: Path) -> None: tester.execute() expected = """\ No caches found """ assert tester.io.fetch_error() == expected ================================================ FILE: tests/console/commands/conftest.py ================================================ from __future__ import annotations import pytest @pytest.fixture def init_basic_inputs() -> str: return "\n".join( [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ] ) @pytest.fixture() def init_basic_toml() -> str: return """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} readme = "README.md" requires-python = ">=3.6" """ @pytest.fixture def init_basic_toml_no_readme(init_basic_toml: str) -> str: # Remove the readme line lines = init_basic_toml.splitlines() lines = [line for line in lines if not line.strip().startswith("readme =")] init_basic_toml_no_readme = "\n".join(lines) return init_basic_toml_no_readme @pytest.fixture() def new_basic_toml() -> str: return """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} readme = "README.md" requires-python = ">=3.6" dependencies = [ ] [tool.poetry] packages = [{include = "my_package", from = "src"}] """ ================================================ FILE: tests/console/commands/debug/__init__.py ================================================ ================================================ FILE: tests/console/commands/debug/test_info.py ================================================ from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING import pytest from poetry.__version__ import __version__ from poetry.utils.env import MockEnv if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from tests.types import CommandTesterFactory @pytest.fixture(autouse=True) def setup(mocker: MockerFixture) -> None: mocker.patch( "poetry.utils.env.EnvManager.get", return_value=MockEnv( path=Path("/prefix"), base=Path("/base/prefix"), is_venv=True ), ) mocker.patch( "sys.prefix", "/poetry/prefix", ) mocker.patch( "sys.executable", "/poetry/prefix/bin/python", ) @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("debug info") def test_debug_info_displays_complete_info(tester: CommandTester) -> None: tester.execute() expected = f""" Poetry Version: {__version__} Python: {".".join(str(v) for v in sys.version_info[:3])} Path: {Path("/poetry/prefix")} Executable: {Path("/poetry/prefix/bin/python")} Virtualenv Python: 3.7.0 Implementation: CPython Path: {Path("/prefix")} Executable: {Path(sys.executable)} Valid: True Base Platform: darwin OS: posix Python: {".".join(str(v) for v in sys.version_info[:3])} Path: {Path("/base/prefix")} Executable: python """ assert tester.io.fetch_output() == expected ================================================ FILE: tests/console/commands/debug/test_resolve.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.factory import Factory from tests.helpers import get_package if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from tests.helpers import TestRepository from tests.types import CommandTesterFactory @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("debug resolve") @pytest.fixture(autouse=True) def __add_packages(repo: TestRepository) -> None: cachy020 = get_package("cachy", "0.2.0") cachy020.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(cachy020) repo.add_package(get_package("msgpack-python", "0.5.3")) repo.add_package(get_package("pendulum", "2.0.3")) repo.add_package(get_package("cleo", "0.6.5")) def test_debug_resolve_gives_resolution_results(tester: CommandTester) -> None: tester.execute("cachy") expected = """\ Resolving dependencies... Resolution results: msgpack-python 0.5.3 cachy 0.2.0 """ assert tester.io.fetch_output() == expected def test_debug_resolve_tree_option_gives_the_dependency_tree( tester: CommandTester, ) -> None: tester.execute("cachy --tree") expected = """\ Resolving dependencies... Resolution results: cachy 0.2.0 └── msgpack-python >=0.5 <0.6 """ assert tester.io.fetch_output() == expected def test_debug_resolve_git_dependency(tester: CommandTester) -> None: tester.execute("git+https://github.com/demo/demo.git") expected = """\ Resolving dependencies... Resolution results: pendulum 2.0.3 demo 0.1.2 """ assert tester.io.fetch_output() == expected ================================================ FILE: tests/console/commands/env/__init__.py ================================================ ================================================ FILE: tests/console/commands/env/conftest.py ================================================ from __future__ import annotations import os from typing import TYPE_CHECKING import pytest from poetry.utils.env import EnvManager if TYPE_CHECKING: from collections.abc import Iterator from pathlib import Path from tests.helpers import PoetryTestApplication @pytest.fixture def venv_name(app: PoetryTestApplication) -> str: return EnvManager.generate_env_name( app.poetry.package.name, str(app.poetry.file.path.parent), ) @pytest.fixture def venv_cache(tmp_path: Path) -> Path: path = tmp_path / "venv_cache" path.mkdir() return path @pytest.fixture(scope="module") def python_versions() -> list[str]: return ["3.6", "3.7"] @pytest.fixture def venvs_in_cache_config(app: PoetryTestApplication, venv_cache: Path) -> None: app.poetry.config.merge({"virtualenvs": {"path": str(venv_cache)}}) @pytest.fixture def venvs_in_cache_dirs( app: PoetryTestApplication, venvs_in_cache_config: None, venv_name: str, venv_cache: Path, python_versions: list[str], ) -> list[str]: directories = [] for version in python_versions: directory = venv_cache / f"{venv_name}-py{version}" directory.mkdir(parents=True, exist_ok=True) directories.append(directory.name) return directories @pytest.fixture def venvs_in_project_dir(app: PoetryTestApplication) -> Iterator[Path]: os.environ.pop("VIRTUAL_ENV", None) venv_dir = app.poetry.file.path.parent.joinpath(".venv") venv_dir.mkdir(exist_ok=True) app.poetry.config.merge({"virtualenvs": {"in-project": True}}) try: yield venv_dir finally: if venv_dir.exists(): venv_dir.rmdir() @pytest.fixture def venvs_in_project_dir_none(app: PoetryTestApplication) -> Iterator[Path]: os.environ.pop("VIRTUAL_ENV", None) venv_dir = app.poetry.file.path.parent.joinpath(".venv") venv_dir.mkdir(exist_ok=True) app.poetry.config.merge({"virtualenvs": {"in-project": None}}) try: yield venv_dir finally: venv_dir.rmdir() @pytest.fixture def venvs_in_project_dir_false(app: PoetryTestApplication) -> Iterator[Path]: os.environ.pop("VIRTUAL_ENV", None) venv_dir = app.poetry.file.path.parent.joinpath(".venv") venv_dir.mkdir(exist_ok=True) app.poetry.config.merge({"virtualenvs": {"in-project": False}}) try: yield venv_dir finally: venv_dir.rmdir() ================================================ FILE: tests/console/commands/env/helpers.py ================================================ from __future__ import annotations import os from pathlib import Path from typing import TYPE_CHECKING from typing import Any from poetry.core.constraints.version import Version if TYPE_CHECKING: from collections.abc import Callable VERSION_3_7_1 = Version.parse("3.7.1") def build_venv(path: Path | str, **_: Any) -> None: Path(path).mkdir(parents=True, exist_ok=True) def check_output_wrapper( version: Version = VERSION_3_7_1, ) -> Callable[[list[str], Any, Any], str]: def check_output(cmd: list[str], *args: Any, **kwargs: Any) -> str: # cmd is a list, like ["python", "-c", "do stuff"] python_cmd = cmd[-1] if "print(json.dumps(env))" in python_cmd: return ( f'{{"version_info": [{version.major}, {version.minor},' f" {version.patch}]}}" ) if "sys.version_info[:3]" in python_cmd: return version.text if "sys.version_info[:2]" in python_cmd: return f"{version.major}.{version.minor}" if "import sys; print(sys.executable)" in python_cmd: executable = cmd[0] basename = os.path.basename(executable) return f"/usr/bin/{basename}" if "print(sys.base_prefix)" in python_cmd: return "/usr" assert "import sys; print(sys.prefix)" in python_cmd return "/prefix" return check_output ================================================ FILE: tests/console/commands/env/test_activate.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.console.commands.env.activate import ShellNotSupportedError from poetry.utils._compat import WINDOWS if TYPE_CHECKING: from cleo.testers.application_tester import ApplicationTester from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.utils.env import VirtualEnv from tests.types import CommandTesterFactory @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("env activate") @pytest.mark.parametrize( "shell, command, ext", ( ("dash", ".", ""), ("bash", "source", ""), ("zsh", "source", ""), ("fish", "source", ".fish"), ("nu", "overlay use", ".nu"), ("csh", "source", ".csh"), ("tcsh", "source", ".csh"), ), ) def test_env_activate_prints_correct_script( tmp_venv: VirtualEnv, mocker: MockerFixture, tester: CommandTester, shell: str, command: str, ext: str, ) -> None: mocker.patch("shellingham.detect_shell", return_value=(shell, None)) mocker.patch("poetry.utils.env.EnvManager.get", return_value=tmp_venv) if WINDOWS and shell in {"csh", "tcsh"}: with pytest.raises(ShellNotSupportedError): tester.execute() else: tester.execute() line = tester.io.fetch_output().rstrip("\n") assert line == f"{command} {tmp_venv.bin_dir.as_posix()}/activate{ext}" @pytest.mark.parametrize( "shell, command, ext", ( ("cmd", "", ".bat"), ("pwsh", "&", ".ps1"), ("powershell", "&", ".ps1"), ), ) @pytest.mark.skipif(not WINDOWS, reason="Only Windows shells") def test_env_activate_prints_correct_script_for_windows_shells( tmp_venv: VirtualEnv, mocker: MockerFixture, tester: CommandTester, shell: str, command: str, ext: str, ) -> None: mocker.patch("shellingham.detect_shell", return_value=(shell, None)) mocker.patch("poetry.utils.env.EnvManager.get", return_value=tmp_venv) tester.execute() line = tester.io.fetch_output().rstrip("\n") activation_script = tmp_venv.bin_dir / f"activate{ext}" assert line == f'{command} "{activation_script}"'.strip() @pytest.mark.parametrize("verbosity", ["", "-v", "-vv", "-vvv"]) def test_no_additional_output_in_verbose_mode( tmp_venv: VirtualEnv, mocker: MockerFixture, app_tester: ApplicationTester, verbosity: str, ) -> None: mocker.patch("shellingham.detect_shell", return_value=("pwsh", None)) mocker.patch("poetry.utils.env.EnvManager.get", return_value=tmp_venv) # use an AppTester instead of a CommandTester to catch additional output app_tester.execute(f"env activate {verbosity}") lines = app_tester.io.fetch_output().splitlines() assert len(lines) == 1 ================================================ FILE: tests/console/commands/env/test_info.py ================================================ from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING import pytest from poetry.utils.env import MockEnv if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from tests.types import CommandTesterFactory @pytest.fixture(autouse=True) def setup(mocker: MockerFixture) -> None: mocker.patch( "poetry.utils.env.EnvManager.get", return_value=MockEnv( path=Path("/prefix"), base=Path("/base/prefix"), is_venv=True ), ) @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("env info") def test_env_info_displays_complete_info(tester: CommandTester) -> None: tester.execute() expected = f""" Virtualenv Python: 3.7.0 Implementation: CPython Path: {Path("/prefix")} Executable: {sys.executable} Valid: True Base Platform: darwin OS: posix Python: {".".join(str(v) for v in sys.version_info[:3])} Path: {Path("/base/prefix")} Executable: python """ assert tester.io.fetch_output() == expected def test_env_info_displays_path_only(tester: CommandTester) -> None: tester.execute("--path") expected = str(Path("/prefix")) + "\n" assert tester.io.fetch_output() == expected def test_env_info_displays_executable_only(tester: CommandTester) -> None: tester.execute("--executable") expected = str(sys.executable) + "\n" assert tester.io.fetch_output() == expected ================================================ FILE: tests/console/commands/env/test_list.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest import tomlkit from poetry.toml.file import TOMLFile if TYPE_CHECKING: from pathlib import Path from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.utils.env import MockEnv from tests.types import CommandTesterFactory @pytest.fixture def venv_activate_37(venv_cache: Path, venv_name: str) -> None: envs_file = TOMLFile(venv_cache / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("env list") def test_none_activated( tester: CommandTester, venvs_in_cache_dirs: list[str], mocker: MockerFixture, env: MockEnv, ) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=env) tester.execute() expected = "\n".join(venvs_in_cache_dirs) assert tester.io.fetch_output().strip() == expected def test_activated( tester: CommandTester, venvs_in_cache_dirs: list[str], venv_cache: Path, venv_activate_37: None, ) -> None: tester.execute() expected = "\n".join(venvs_in_cache_dirs).replace("py3.7", "py3.7 (Activated)") assert tester.io.fetch_output().strip() == expected def test_in_project_venv( tester: CommandTester, venvs_in_project_dir: list[str] ) -> None: tester.execute() expected = ".venv (Activated)\n" assert tester.io.fetch_output() == expected def test_in_project_venv_no_explicit_config( tester: CommandTester, venvs_in_project_dir_none: list[str] ) -> None: tester.execute() expected = ".venv (Activated)\n" assert tester.io.fetch_output() == expected def test_in_project_venv_is_false( tester: CommandTester, venvs_in_project_dir_false: list[str] ) -> None: tester.execute() expected = "" assert tester.io.fetch_output() == expected ================================================ FILE: tests/console/commands/env/test_remove.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.core.constraints.version import Version from tests.console.commands.env.helpers import check_output_wrapper if TYPE_CHECKING: from pathlib import Path from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from tests.types import CommandTesterFactory @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("env remove") def test_remove_by_python_version( mocker: MockerFixture, tester: CommandTester, venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, ) -> None: check_output = mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) tester.execute("3.6") assert check_output.called assert not (venv_cache / f"{venv_name}-py3.6").exists() expected = f"Deleted virtualenv: {venv_cache / venv_name}-py3.6\n" assert tester.io.fetch_output() == expected def test_remove_by_name( tester: CommandTester, venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, ) -> None: expected = "" for name in venvs_in_cache_dirs: tester.execute(name) assert not (venv_cache / name).exists() expected += f"Deleted virtualenv: {venv_cache / name}\n" assert tester.io.fetch_output() == expected @pytest.mark.parametrize( "envs_file", [None, "empty", "self", "other", "self_and_other"] ) def test_remove_all( tester: CommandTester, venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, envs_file: str | None, ) -> None: envs_file_path = venv_cache / "envs.toml" if envs_file == "empty": envs_file_path.touch() elif envs_file == "self": envs_file_path.write_text( f'[{venv_name}]\nminor = "3.9"\npatch = "3.9.1"\n', encoding="utf-8" ) elif envs_file == "other": envs_file_path.write_text( '[other-abcdefgh]\nminor = "3.9"\npatch = "3.9.1"\n', encoding="utf-8" ) elif envs_file == "self_and_other": envs_file_path.write_text( f'[{venv_name}]\nminor = "3.9"\npatch = "3.9.1"\n' '[other-abcdefgh]\nminor = "3.9"\npatch = "3.9.1"\n', encoding="utf-8", ) else: # no envs file -> nothing to prepare assert envs_file is None expected = {""} tester.execute("--all") for name in venvs_in_cache_dirs: assert not (venv_cache / name).exists() expected.add(f"Deleted virtualenv: {venv_cache / name}") assert set(tester.io.fetch_output().split("\n")) == expected if envs_file is not None: assert envs_file_path.exists() envs_file_content = envs_file_path.read_text(encoding="utf-8") assert venv_name not in envs_file_content if "other" in envs_file: assert "other-abcdefgh" in envs_file_content else: assert envs_file_content == "" else: assert not envs_file_path.exists() def test_remove_all_and_version( tester: CommandTester, venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, ) -> None: expected = {""} tester.execute(f"--all {venvs_in_cache_dirs[0]}") for name in venvs_in_cache_dirs: assert not (venv_cache / name).exists() expected.add(f"Deleted virtualenv: {venv_cache / name}") assert set(tester.io.fetch_output().split("\n")) == expected def test_remove_multiple( tester: CommandTester, venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, ) -> None: expected = {""} removed_envs = venvs_in_cache_dirs[0:2] remaining_envs = venvs_in_cache_dirs[2:] tester.execute(" ".join(removed_envs)) for name in removed_envs: assert not (venv_cache / name).exists() expected.add(f"Deleted virtualenv: {venv_cache / name}") for name in remaining_envs: assert (venv_cache / name).exists() assert set(tester.io.fetch_output().split("\n")) == expected def test_remove_in_project(tester: CommandTester, venvs_in_project_dir: Path) -> None: assert venvs_in_project_dir.exists() tester.execute() assert not venvs_in_project_dir.exists() expected = f"Deleted virtualenv: {venvs_in_project_dir}\n" assert tester.io.fetch_output() == expected def test_remove_in_project_all( tester: CommandTester, venvs_in_project_dir: Path ) -> None: assert venvs_in_project_dir.exists() tester.execute("--all") assert not venvs_in_project_dir.exists() expected = f"Deleted virtualenv: {venvs_in_project_dir}\n" assert tester.io.fetch_output() == expected ================================================ FILE: tests/console/commands/env/test_use.py ================================================ from __future__ import annotations import os from pathlib import Path from typing import TYPE_CHECKING from typing import Any import pytest import tomlkit from poetry.core.constraints.version import Version from poetry.console.commands.env.use import EnvUseCommand from poetry.toml.file import TOMLFile from poetry.utils.env import MockEnv from poetry.utils.env.python.exceptions import NoCompatiblePythonVersionFoundError from tests.console.commands.env.helpers import build_venv from tests.console.commands.env.helpers import check_output_wrapper if TYPE_CHECKING: from unittest.mock import MagicMock from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.utils.env.base_env import PythonVersion from tests.types import CommandTesterFactory from tests.types import MockedPythonRegister @pytest.fixture(autouse=True) def setup(mocker: MockerFixture) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] @pytest.fixture(autouse=True) def mock_subprocess_calls( setup: None, current_python: PythonVersion, mocker: MockerFixture ) -> None: mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.from_parts(*current_python[:3])), ) mocker.patch( "subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)], ) @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("env use") def test_activate_activates_non_existing_virtualenv_no_envs_file( mocker: MockerFixture, tester: CommandTester, venv_cache: Path, venv_name: str, venvs_in_cache_config: None, mocked_python_register: MockedPythonRegister, with_no_active_python: MagicMock, ) -> None: mocked_python_register("3.7.1") mock_build_env = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=build_venv ) envs_file = TOMLFile(venv_cache / "envs.toml") assert not envs_file.exists() assert not list(venv_cache.iterdir()) tester.execute("3.7") venv_py37 = venv_cache / f"{venv_name}-py3.7" mock_build_env.assert_called_with( venv_py37, executable=Path("/usr/bin/python3.7"), flags={ "always-copy": False, "system-site-packages": False, "no-pip": False, }, prompt="simple-project-py3.7", ) assert envs_file.exists() envs: dict[str, Any] = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert ( tester.io.fetch_error() == f"Creating virtualenv {venv_py37.name} in {venv_py37.parent}\n" ) assert tester.io.fetch_output() == f"Using virtualenv: {venv_py37}\n" @pytest.mark.parametrize("use_poetry_python", [True, False]) def test_activate_does_not_activate_non_existing_virtualenv_with_unsupported_version( tester: CommandTester, venv_cache: Path, venv_name: str, venvs_in_cache_config: None, mocked_python_register: MockedPythonRegister, with_no_active_python: MagicMock, use_poetry_python: bool, ) -> None: mocked_python_register("3.7.1") mocked_python_register("3.8.2") command = tester.command assert isinstance(command, EnvUseCommand) command.poetry.package.python_versions = "~3.8" command.poetry.config.merge( {"virtualenvs": {"use-poetry-python": use_poetry_python}} ) assert not list(venv_cache.iterdir()) with pytest.raises(NoCompatiblePythonVersionFoundError): tester.execute("3.7") assert not list(venv_cache.iterdir()) def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( tester: CommandTester, current_python: PythonVersion, venv_cache: Path, venv_name: str, venvs_in_cache_config: None, mocked_python_register: MockedPythonRegister, ) -> None: os.environ["VIRTUAL_ENV"] = "/environment/prefix" python_minor = ".".join(str(v) for v in current_python[:2]) python_patch = ".".join(str(v) for v in current_python[:3]) venv_dir = venv_cache / f"{venv_name}-py{python_minor}" venv_dir.mkdir(parents=True, exist_ok=True) envs_file = TOMLFile(venv_cache / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": python_minor, "patch": python_patch} envs_file.write(doc) mocked_python_register(python_patch) tester.execute(python_minor) expected = f"""\ Using virtualenv: {venv_dir} """ assert tester.io.fetch_output() == expected def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var( mocker: MockerFixture, tester: CommandTester, current_python: PythonVersion, venv_cache: Path, venv_name: str, venvs_in_cache_config: None, mocked_python_register: MockedPythonRegister, ) -> None: os.environ["VIRTUAL_ENV"] = "/environment/prefix" python_minor = ".".join(str(v) for v in current_python[:2]) venv_dir = venv_cache / f"{venv_name}-py{python_minor}" mocked_python_register(python_minor) mocker.patch( "poetry.utils.env.EnvManager._env", new_callable=mocker.PropertyMock, return_value=MockEnv( path=Path("/environment/prefix"), base=Path("/base/prefix"), version_info=current_python, is_venv=True, ), ) mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) tester.execute(python_minor) assert ( tester.io.fetch_error() == f"Creating virtualenv {venv_dir.name} in {venv_dir.parent}\n" ) assert tester.io.fetch_output() == f"Using virtualenv: {venv_dir}\n" ================================================ FILE: tests/console/commands/python/__init__.py ================================================ ================================================ FILE: tests/console/commands/python/test_python_install.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.core.constraints.version.version import Version from poetry.console.exceptions import PoetryRuntimeError from poetry.utils.env.python.installer import PythonDownloadNotFoundError from poetry.utils.env.python.installer import PythonInstallationError if TYPE_CHECKING: from unittest.mock import MagicMock from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.config.config import Config from tests.types import CommandTesterFactory @pytest.fixture(autouse=True) def mock_installer(mocker: MockerFixture) -> MagicMock: return mocker.patch("poetry.console.commands.python.install.PythonInstaller") @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("python install") def test_install_invalid_version(tester: CommandTester) -> None: tester.execute("foo") assert tester.status_code == 1 assert tester.io.fetch_error() == "Invalid Python version requested foo\n" def test_install_free_threaded_not_supported(tester: CommandTester) -> None: tester.execute("-t 3.12") assert tester.status_code == 1 assert ( "Free threading is not supported for Python versions prior to 3.13.0.\n" in tester.io.fetch_error() ) def test_install_exists(tester: CommandTester, mock_installer: MagicMock) -> None: mock_installer.return_value.exists.return_value = True tester.execute("3.11") mock_installer.assert_called_once_with("3.11", "cpython", False) mock_installer.return_value.install.assert_not_called() assert tester.status_code == 1 assert "Python version already installed at" in tester.io.fetch_error() def test_install_no_download(tester: CommandTester, mock_installer: MagicMock) -> None: mock_installer.return_value.exists.side_effect = PythonDownloadNotFoundError tester.execute("3.11") mock_installer.assert_called_once_with("3.11", "cpython", False) mock_installer.return_value.install.assert_not_called() assert tester.status_code == 1 assert ( "No suitable standalone build found for the requested Python version.\n" in tester.io.fetch_error() ) def test_install_failure(tester: CommandTester, mock_installer: MagicMock) -> None: mock_installer.return_value.exists.return_value = False mock_installer.return_value.install.side_effect = PythonInstallationError("foo") tester.execute("3.11") mock_installer.assert_called_once_with("3.11", "cpython", False) mock_installer.return_value.install.assert_called_once() assert tester.status_code == 1 assert ( tester.io.fetch_output() == "Downloading and installing 3.11 (cpython) ... Failed\n" ) assert "foo\n" in tester.io.fetch_error() @pytest.mark.parametrize("clean", [False, True]) def test_install_corrupt( tester: CommandTester, mock_installer: MagicMock, config: Config, clean: bool ) -> None: def create_install_dir() -> None: (config.python_installation_dir / "cpython@3.11.9").mkdir(parents=True) mock_installer.return_value.exists.side_effect = [False, PoetryRuntimeError("foo")] mock_installer.return_value.install.side_effect = create_install_dir mock_installer.return_value.version = Version.parse("3.11.9") with pytest.raises(PoetryRuntimeError): clean_opt = "-c " if clean else "" tester.execute(f"{clean_opt}3.11") mock_installer.assert_called_once_with("3.11", "cpython", False) mock_installer.return_value.install.assert_called_once() expected = ( "Downloading and installing 3.11 (cpython) ... Done\n" "Testing 3.11 (cpython) ... Failed\n" ) if clean: expected += "Removing installation 3.11.9 (cpython) ... Done\n" assert tester.io.fetch_output() == expected def test_install_success(tester: CommandTester, mock_installer: MagicMock) -> None: mock_installer.return_value.exists.return_value = False tester.execute("3.11") mock_installer.assert_called_once_with("3.11", "cpython", False) mock_installer.return_value.install.assert_called_once() assert tester.status_code == 0 assert tester.io.fetch_output() == ( "Downloading and installing 3.11 (cpython) ... Done\n" "Testing 3.11 (cpython) ... Done\n" ) def test_install_reinstall(tester: CommandTester, mock_installer: MagicMock) -> None: mock_installer.return_value.exists.return_value = True tester.execute("-r 3.11") mock_installer.assert_called_once_with("3.11", "cpython", False) mock_installer.return_value.install.assert_called_once() assert tester.status_code == 0 assert tester.io.fetch_output() == ( "Downloading and installing 3.11 (cpython) ... Done\n" "Testing 3.11 (cpython) ... Done\n" ) @pytest.mark.parametrize("free_threaded", [False, True]) @pytest.mark.parametrize("implementation", ["cpython", "pypy"]) def test_install_passes_options_to_installer( tester: CommandTester, mock_installer: MagicMock, free_threaded: bool, implementation: str, ) -> None: mock_installer.return_value.exists.return_value = False free_threaded_opt = "-t " if free_threaded else "" impl_opt = f"-i {implementation} " tester.execute(f"{free_threaded_opt}{impl_opt}3.13") mock_installer.assert_called_once_with("3.13", implementation, free_threaded) mock_installer.return_value.install.assert_called_once() assert tester.status_code == 0 details = f"{implementation}, free-threaded" if free_threaded else implementation assert tester.io.fetch_output() == ( f"Downloading and installing 3.13 ({details}) ... Done\n" f"Testing 3.13 ({details}) ... Done\n" ) def test_install_free_threaded_via_trailing_t( tester: CommandTester, mock_installer: MagicMock ) -> None: mock_installer.return_value.exists.return_value = False tester.execute("3.13t") mock_installer.assert_called_once_with("3.13", "cpython", True) mock_installer.return_value.install.assert_called_once() assert tester.status_code == 0 assert tester.io.fetch_output() == ( "Downloading and installing 3.13 (cpython, free-threaded) ... Done\n" "Testing 3.13 (cpython, free-threaded) ... Done\n" ) ================================================ FILE: tests/console/commands/python/test_python_list.py ================================================ from __future__ import annotations import platform from typing import TYPE_CHECKING import pytest from poetry.utils._compat import WINDOWS from tests.helpers import pbs_installer_supported_arch if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from poetry.config.config import Config from tests.types import CommandTesterFactory from tests.types import MockedPoetryPythonRegister from tests.types import MockedPythonRegister @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("python list") def test_list_no_versions(tester: CommandTester) -> None: tester.execute() assert tester.io.fetch_output() == "No Python installations found.\n" def test_list_all(tester: CommandTester) -> None: tester.execute("--all") if platform.system() == "FreeBSD" or not pbs_installer_supported_arch( platform.machine() ): assert tester.io.fetch_output() == "No Python installations found.\n" else: assert "Available for download" in tester.io.fetch_output() def test_list_invalid_version(tester: CommandTester) -> None: tester.execute("foo") assert tester.status_code == 1 assert tester.io.fetch_error() == "Invalid Python version requested foo\n" def test_list( tester: CommandTester, mocked_python_register: MockedPythonRegister ) -> None: mocked_python_register("3.9.1", parent="a") mocked_python_register("3.9.3", parent="b") mocked_python_register("3.10.4", parent="c") tester.execute() expected = """\ Version Implementation Manager Path \ 3.10.4 CPython System c/python3.10 \ 3.9.3 CPython System b/python3.9 \ 3.9.1 CPython System a/python3.9 \ """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize("only_poetry_managed", [False, True]) def test_list_poetry_managed( tester: CommandTester, config: Config, mocked_poetry_managed_python_register: MockedPoetryPythonRegister, only_poetry_managed: bool, ) -> None: mocked_poetry_managed_python_register("3.9.1", "cpython") mocked_poetry_managed_python_register("3.10.8", "pypy") mocked_poetry_managed_python_register("3.14.0", "cpython", free_threaded=True) tester.execute("-m" if only_poetry_managed else "") lines = tester.io.fetch_output().splitlines() system_lines = [line.strip() for line in lines if "System" in line] poetry_lines = [line.strip() for line in lines if "Poetry" in line] install_dir = config.python_installation_dir.as_posix() bin_dir = "" if WINDOWS else "bin/" expected = { f"3.10.8 PyPy Poetry {install_dir}/pypy@3.10.8/{bin_dir}pypy", f"3.10.8 PyPy Poetry {install_dir}/pypy@3.10.8/{bin_dir}python", f"3.9.1 CPython Poetry {install_dir}/cpython@3.9.1/{bin_dir}python", f"3.14.0t CPython Poetry {install_dir}/cpython@3.14.0t/{bin_dir}python", } assert set(poetry_lines) == expected if only_poetry_managed: assert not system_lines else: assert system_lines @pytest.mark.parametrize( ("version", "expected"), [("3", 3), ("3.9", 2), ("3.9.2", 0), ("3.9.3", 1)], ) def test_list_version( tester: CommandTester, mocked_python_register: MockedPythonRegister, version: str, expected: int, ) -> None: mocked_python_register("2.7.13", parent="_") mocked_python_register("3.9.1", parent="a") mocked_python_register("3.9.3", parent="b") mocked_python_register("3.10.4", parent="c") tester.execute(version) assert len(tester.io.fetch_output().splitlines()) - 1 == expected @pytest.mark.parametrize( ("implementation", "expected"), [("PyPy", 1), ("pypy", 1), ("CPython", 2)] ) def test_list_implementation( tester: CommandTester, mocked_python_register: MockedPythonRegister, implementation: str, expected: int, ) -> None: mocked_python_register("3.9.1", implementation="PyPy", parent="a") mocked_python_register("3.9.3", implementation="CPython", parent="b") mocked_python_register("3.10.4", implementation="CPython", parent="c") tester.execute(f"-i {implementation}") assert len(tester.io.fetch_output().splitlines()) - 1 == expected @pytest.mark.parametrize(("free_threaded", "expected"), [("-t", 1), ("", 3)]) def test_list_free_threaded( tester: CommandTester, mocked_python_register: MockedPythonRegister, free_threaded: str, expected: int, ) -> None: mocked_python_register("3.13.0", free_threaded=False, parent="a") mocked_python_register("3.14.0", free_threaded=False, parent="b") mocked_python_register("3.14.0", free_threaded=True, parent="c") tester.execute(free_threaded) assert len(tester.io.fetch_output().splitlines()) - 1 == expected ================================================ FILE: tests/console/commands/python/test_python_remove.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from poetry.config.config import Config from tests.types import CommandTesterFactory from tests.types import MockedPoetryPythonRegister @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("python remove") def test_remove_invalid_version(tester: CommandTester) -> None: tester.execute("foo") assert tester.status_code == 1 assert tester.io.fetch_error() == "Invalid Python version requested foo\n" def test_remove_version_not_precise_enough(tester: CommandTester) -> None: tester.execute("3.9") assert tester.status_code == 1 assert ( tester.io.fetch_error() == """\ Invalid Python version requested 3.9 You need to provide an exact Python version in the format X.Y.Z to be removed. You can use poetry python list -m to list installed Poetry managed Python versions. """ ) def test_remove_version_no_installation(tester: CommandTester, config: Config) -> None: tester.execute("3.9.1") location = config.python_installation_dir / "cpython@3.9.1" assert tester.io.fetch_output() == f"No installation was found at {location}.\n" def test_remove_version( tester: CommandTester, config: Config, mocked_poetry_managed_python_register: MockedPoetryPythonRegister, ) -> None: cpython_path = mocked_poetry_managed_python_register("3.9.1", "cpython") other_cpython_path = mocked_poetry_managed_python_register("3.9.2", "cpython") pypy_path = mocked_poetry_managed_python_register("3.9.1", "pypy") assert tester.execute("3.9.1") == 0, tester.io.fetch_error() assert ( tester.io.fetch_output() == "Removing installation 3.9.1 (cpython) ... Done\n" ) assert not cpython_path.exists() assert pypy_path.exists() assert other_cpython_path.exists() @pytest.mark.parametrize("implementation", ["cpython", "pypy"]) def test_remove_version_implementation( tester: CommandTester, config: Config, mocked_poetry_managed_python_register: MockedPoetryPythonRegister, implementation: str, ) -> None: cpython_path = mocked_poetry_managed_python_register("3.9.1", "cpython") pypy_path = mocked_poetry_managed_python_register("3.9.1", "pypy") assert tester.execute(f"3.9.1 -i {implementation}") == 0, tester.io.fetch_error() assert ( tester.io.fetch_output() == f"Removing installation 3.9.1 ({implementation}) ... Done\n" ) if implementation == "cpython": assert not cpython_path.exists() assert pypy_path.exists() else: assert cpython_path.exists() assert not pypy_path.exists() @pytest.mark.parametrize("free_threaded", [True, False]) @pytest.mark.parametrize("option", [True, False]) def test_remove_version_free_threaded( tester: CommandTester, config: Config, mocked_poetry_managed_python_register: MockedPoetryPythonRegister, free_threaded: bool, option: bool, ) -> None: standard_path = mocked_poetry_managed_python_register("3.14.0", "cpython") free_threaded_path = mocked_poetry_managed_python_register( "3.14.0", "cpython", free_threaded=True ) args = "3.14.0" if free_threaded: args += " --free-threaded" if option else "t" assert tester.execute(args) == 0, tester.io.fetch_error() details = "cpython" if free_threaded: details += ", free-threaded" assert ( tester.io.fetch_output() == f"Removing installation 3.14.0 ({details}) ... Done\n" ) if free_threaded: assert not free_threaded_path.exists() assert standard_path.exists() else: assert not standard_path.exists() assert free_threaded_path.exists() def test_remove_multiple_versions( tester: CommandTester, config: Config, mocked_poetry_managed_python_register: MockedPoetryPythonRegister, ) -> None: cpython_path_1 = mocked_poetry_managed_python_register("3.9.1", "cpython") cpython_path_2 = mocked_poetry_managed_python_register("3.9.2", "cpython") cpython_path_3 = mocked_poetry_managed_python_register("3.9.3", "cpython") tester.execute("3.9.1 3.9.3") assert tester.io.fetch_output() == ( "Removing installation 3.9.1 (cpython) ... Done\n" "Removing installation 3.9.3 (cpython) ... Done\n" ) assert not cpython_path_1.exists() assert cpython_path_2.exists() assert not cpython_path_3.exists() ================================================ FILE: tests/console/commands/self/__init__.py ================================================ ================================================ FILE: tests/console/commands/self/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any import pytest from poetry.core.packages.package import Package from poetry.__version__ import __version__ from poetry.factory import Factory from poetry.repositories import RepositoryPool from poetry.utils.env import EnvManager if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterable import responses from cleo.io.io import IO from pytest_mock import MockerFixture from poetry.config.config import Config from poetry.repositories.repository import Repository from poetry.utils.env import VirtualEnv from tests.helpers import TestRepository @pytest.fixture def poetry_package() -> Package: return Package("poetry", __version__) @pytest.fixture(autouse=True) def _patch_repos( repo: TestRepository, installed: Repository, poetry_package: Package ) -> None: repo.add_package(poetry_package) installed.add_package(poetry_package) @pytest.fixture() def pool(repo: TestRepository) -> RepositoryPool: return RepositoryPool([repo]) def create_pool_factory( repo: Repository, ) -> Callable[[Config, Iterable[dict[str, Any]], IO, bool], RepositoryPool]: def _create_pool( config: Config, sources: Iterable[dict[str, Any]] = (), io: IO | None = None, disable_cache: bool = False, ) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(repo) return pool return _create_pool @pytest.fixture(autouse=True) def setup_mocks( mocker: MockerFixture, tmp_venv: VirtualEnv, installed: Repository, pool: RepositoryPool, http: responses.RequestsMock, repo: Repository, ) -> None: mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) mocker.patch( "poetry.repositories.repository_pool.RepositoryPool.find_packages", pool.find_packages, ) mocker.patch( "poetry.repositories.repository_pool.RepositoryPool.package", pool.package ) mocker.patch( "poetry.installation.installer.Installer._get_installed", return_value=installed, ) mocker.patch.object(Factory, "create_pool", side_effect=create_pool_factory(repo)) ================================================ FILE: tests/console/commands/self/fixtures/poetry-1.0.5-darwin.sha256sum ================================================ be3d3b916cb47038899d6ff37e875fd08ba3fed22bcdbf5a92f3f48fd2f15da8 ================================================ FILE: tests/console/commands/self/test_add_plugins.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.core.packages.package import Package from poetry.console.commands.add import AddCommand from poetry.console.commands.self.self_command import SelfCommand from poetry.factory import Factory from tests.console.commands.self.utils import get_self_command_dependencies if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from tests.helpers import TestRepository from tests.types import CommandTesterFactory @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("self add") def assert_plugin_add_result( tester: CommandTester, expected: str, constraint: str, ) -> None: assert tester.io.fetch_output() == expected dependencies: list[str] | None = get_self_command_dependencies() assert dependencies assert "poetry-plugin" in dependencies[0] assert constraint in dependencies[0] def test_add_no_constraint( tester: CommandTester, repo: TestRepository, ) -> None: repo.add_package(Package("poetry-plugin", "0.1.0")) tester.execute("poetry-plugin") expected = """\ Using version ^0.1.0 for poetry-plugin Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing poetry-plugin (0.1.0) Writing lock file """ assert_plugin_add_result(tester, expected, "(>=0.1.0,<0.2.0)") def test_add_with_constraint( tester: CommandTester, repo: TestRepository, ) -> None: repo.add_package(Package("poetry-plugin", "0.1.0")) repo.add_package(Package("poetry-plugin", "0.2.0")) tester.execute("poetry-plugin@^0.2.0") expected = """ Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing poetry-plugin (0.2.0) Writing lock file """ assert_plugin_add_result(tester, expected, "(>=0.2.0,<0.3.0)") def test_add_with_git_constraint( tester: CommandTester, repo: TestRepository, ) -> None: repo.add_package(Package("pendulum", "2.0.5")) tester.execute("git+https://github.com/demo/poetry-plugin.git") expected = """ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (2.0.5) - Installing poetry-plugin (0.1.2 9cf87a2) Writing lock file """ assert_plugin_add_result( tester, expected, "https://github.com/demo/poetry-plugin.git" ) def test_add_with_git_constraint_with_extras( tester: CommandTester, repo: TestRepository, ) -> None: repo.add_package(Package("pendulum", "2.0.5")) repo.add_package(Package("tomlkit", "0.7.0")) tester.execute("git+https://github.com/demo/poetry-plugin.git[foo]") expected = """ Updating dependencies Resolving dependencies... Package operations: 3 installs, 0 updates, 0 removals - Installing pendulum (2.0.5) - Installing tomlkit (0.7.0) - Installing poetry-plugin (0.1.2 9cf87a2) Writing lock file """ assert_plugin_add_result( tester, expected, "poetry-plugin[foo] @ git+https://github.com/demo/poetry-plugin.git", ) @pytest.mark.parametrize( "url, rev", [ ("git+https://github.com/demo/poetry-plugin2.git#subdirectory=subdir", None), ( "git+https://github.com/demo/poetry-plugin2.git@master#subdirectory=subdir", "master", ), ], ) def test_add_with_git_constraint_with_subdirectory( url: str, rev: str | None, tester: CommandTester, repo: TestRepository, ) -> None: repo.add_package(Package("pendulum", "2.0.5")) tester.execute(url) expected = """ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (2.0.5) - Installing poetry-plugin (0.1.2 9cf87a2) Writing lock file """ assert_plugin_add_result(tester, expected, url) def test_add_existing_plugin_warns_about_no_operation( tester: CommandTester, repo: TestRepository, installed: TestRepository, ) -> None: pyproject = SelfCommand.get_default_system_pyproject_file() with open(pyproject, "w", encoding="utf-8", newline="") as f: f.write( f"""\ [tool.poetry] name = "poetry-instance" version = "1.2.0" description = "Python dependency management and packaging made easy." authors = [] [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.{SelfCommand.ADDITIONAL_PACKAGE_GROUP}.dependencies] poetry-plugin = "^1.2.3" """ ) installed.add_package(Package("poetry-plugin", "1.2.3")) repo.add_package(Package("poetry-plugin", "1.2.3")) tester.execute("poetry-plugin") assert isinstance(tester.command, AddCommand) expected = f"""\ The following packages are already present in the pyproject.toml and will be\ skipped: - poetry-plugin {tester.command._hint_update_packages} Nothing to add. """ assert tester.io.fetch_output() == expected def test_add_existing_plugin_updates_if_requested( tester: CommandTester, repo: TestRepository, installed: TestRepository, ) -> None: pyproject = SelfCommand.get_default_system_pyproject_file() with open(pyproject, "w", encoding="utf-8", newline="") as f: f.write( f"""\ [tool.poetry] name = "poetry-instance" version = "1.2.0" description = "Python dependency management and packaging made easy." authors = [] [tool.poetry.dependencies] python = "^3.6" [dependency-groups] {SelfCommand.ADDITIONAL_PACKAGE_GROUP} = [ "poetry-plugin (>=1.2.3,<2.0.0)" ] """ ) installed.add_package(Package("poetry-plugin", "1.2.3")) repo.add_package(Package("poetry-plugin", "1.2.3")) repo.add_package(Package("poetry-plugin", "2.3.4")) tester.execute("poetry-plugin@latest") expected = """\ Using version ^2.3.4 for poetry-plugin Updating dependencies Resolving dependencies... Package operations: 0 installs, 1 update, 0 removals - Updating poetry-plugin (1.2.3 -> 2.3.4) Writing lock file """ assert_plugin_add_result(tester, expected, "(>=2.3.4,<3.0.0)") def test_adding_a_plugin_can_update_poetry_dependencies_if_needed( tester: CommandTester, repo: TestRepository, installed: TestRepository, poetry_package: Package, ) -> None: poetry_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.0")) plugin_package = Package("poetry-plugin", "1.2.3") plugin_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.2")) installed.add_package(poetry_package) installed.add_package(Package("tomlkit", "0.7.1")) repo.add_package(plugin_package) repo.add_package(Package("tomlkit", "0.7.1")) repo.add_package(Package("tomlkit", "0.7.2")) tester.execute("poetry-plugin") expected = """\ Using version ^1.2.3 for poetry-plugin Updating dependencies Resolving dependencies... Package operations: 1 install, 1 update, 0 removals - Updating tomlkit (0.7.1 -> 0.7.2) - Installing poetry-plugin (1.2.3) Writing lock file """ assert_plugin_add_result(tester, expected, "(>=1.2.3,<2.0.0)") ================================================ FILE: tests/console/commands/self/test_install.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.console.commands.self.install import SelfInstallCommand if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from tests.types import CommandTesterFactory @pytest.fixture def command() -> str: return "self install" @pytest.fixture def tester(command_tester_factory: CommandTesterFactory, command: str) -> CommandTester: return command_tester_factory(command) @pytest.mark.parametrize( "pyproject_content", ( None, """\ [tool.poetry] name = "poetry-instance" version = "1.2" description = "" authors = [] license = "" # no package-mode -> defaults to true [tool.poetry.dependencies] python = "3.9" poetry = "1.2" """, ), ) def test_self_install( tester: CommandTester, pyproject_content: str | None, ) -> None: command = tester.command assert isinstance(command, SelfInstallCommand) pyproject_path = command.system_pyproject if pyproject_content: pyproject_path.write_text(pyproject_content, encoding="utf-8") else: assert not pyproject_path.exists() tester.execute() output = tester.io.fetch_output() assert output.startswith("Updating dependencies") assert output.endswith("Writing lock file\n") assert tester.io.fetch_error() == "" @pytest.mark.parametrize("sync", [True, False]) def test_sync_deprecation(tester: CommandTester, sync: bool) -> None: tester.execute("--sync" if sync else "") error = tester.io.fetch_error() if sync: assert "deprecated" in error assert "poetry self sync" in error else: assert error == "" ================================================ FILE: tests/console/commands/self/test_remove_plugins.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest import tomlkit from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.__version__ import __version__ from poetry.console.commands.self.self_command import SelfCommand from poetry.factory import Factory from tests.console.commands.self.utils import get_self_command_dependencies if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from poetry.repositories import Repository from tests.types import CommandTesterFactory @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("self remove") @pytest.fixture(autouse=True) def install_plugin(installed: Repository) -> None: package = ProjectPackage("poetry-instance", __version__) plugin = Package("poetry-plugin", "1.2.3") content = Factory.create_legacy_pyproject_from_package(package) content["dependency-groups"] = tomlkit.table() content["dependency-groups"][SelfCommand.ADDITIONAL_PACKAGE_GROUP] = tomlkit.array( # type: ignore[index] "[\n]" ) content["dependency-groups"][SelfCommand.ADDITIONAL_PACKAGE_GROUP].append( # type: ignore[index, union-attr, call-arg] Dependency(plugin.name, "^1.2.3").to_pep_508() ) system_pyproject_file = SelfCommand.get_default_system_pyproject_file() with open(system_pyproject_file, "w", encoding="utf-8", newline="") as f: f.write(content.as_string()) lock_content = { "package": [ { "name": "poetry-plugin", "version": "1.2.3", "optional": False, "platform": "*", "python-versions": "*", "files": [], }, ], "metadata": { "lock-version": "2.0", "python-versions": "^3.6", "content-hash": "123456789", }, } system_pyproject_file.parent.joinpath("poetry.lock").write_text( tomlkit.dumps(lock_content), encoding="utf-8" ) installed.add_package(plugin) def test_remove_installed_package(tester: CommandTester) -> None: tester.execute("poetry-plugin") expected = """\ Updating dependencies Resolving dependencies... Package operations: 0 installs, 0 updates, 1 removal - Removing poetry-plugin (1.2.3) Writing lock file """ assert tester.io.fetch_output() == expected dependencies = get_self_command_dependencies() assert not dependencies def test_remove_installed_package_dry_run(tester: CommandTester) -> None: tester.execute("poetry-plugin --dry-run") expected = f"""\ Updating dependencies Resolving dependencies... Package operations: 0 installs, 0 updates, 1 removal, 1 skipped - Removing poetry-plugin (1.2.3) - Installing poetry ({__version__}): Skipped for the following reason: Already \ installed """ assert tester.io.fetch_output() == expected dependencies = get_self_command_dependencies() assert dependencies assert len(dependencies) == 1 assert "poetry-plugin" in dependencies[0] ================================================ FILE: tests/console/commands/self/test_self_command.py ================================================ from __future__ import annotations import pytest from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.__version__ import __version__ from poetry.console.commands.self.self_command import SelfCommand from poetry.factory import Factory @pytest.fixture def example_system_pyproject() -> str: package = ProjectPackage("poetry-instance", __version__) plugin = Package("poetry-plugin", "1.2.3") package.add_dependency( Dependency(plugin.name, "^1.2.3", groups=[SelfCommand.ADDITIONAL_PACKAGE_GROUP]) ) content = Factory.create_legacy_pyproject_from_package(package) return content.as_string().rstrip("\n") @pytest.mark.parametrize("existing_newlines", [0, 2]) def test_generate_system_pyproject_trailing_newline( existing_newlines: int, example_system_pyproject: str, ) -> None: cmd = SelfCommand() cmd.system_pyproject.write_text( example_system_pyproject + "\n" * existing_newlines, encoding="utf-8" ) cmd.generate_system_pyproject() generated = cmd.system_pyproject.read_text(encoding="utf-8") assert len(generated) - len(generated.rstrip("\n")) == existing_newlines def test_generate_system_pyproject_carriage_returns( example_system_pyproject: str, ) -> None: cmd = SelfCommand() cmd.system_pyproject.write_text(example_system_pyproject + "\n", encoding="utf-8") cmd.generate_system_pyproject() with open( cmd.system_pyproject, newline="", encoding="utf-8" ) as f: # do not translate newlines generated = f.read() assert "\r\r" not in generated ================================================ FILE: tests/console/commands/self/test_show.py ================================================ from __future__ import annotations import json from typing import TYPE_CHECKING import pytest import tomlkit from poetry.__version__ import __version__ from poetry.console.commands.self.self_command import SelfCommand if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from tests.types import CommandTesterFactory @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("self show") @pytest.mark.parametrize("options", ["", "--format json", "--format text"]) def test_show_format(tester: CommandTester, options: str) -> None: pyproject_content = { "tool": { "poetry": { "name": "poetry-instance", "version": __version__, "dependencies": {"python": "^3.9", "poetry": __version__}, } } } lock_content = { "package": [ { "name": "poetry", "version": __version__, "optional": False, "platform": "*", "python-versions": "*", "files": [], }, ], "metadata": { "lock-version": "2.0", "python-versions": "^3.9", "content-hash": "123456789", }, } if "json" in options: expected = json.dumps( [ { "name": "poetry", "installed_status": "installed", "version": __version__, "description": "", } ] ) else: expected = f"poetry {__version__}" system_pyproject_file = SelfCommand.get_default_system_pyproject_file() system_pyproject_file.write_text(tomlkit.dumps(pyproject_content), encoding="utf-8") system_pyproject_file.parent.joinpath("poetry.lock").write_text( tomlkit.dumps(lock_content), encoding="utf-8" ) assert tester.execute(options) == 0 assert tester.io.fetch_output().strip() == expected def test_self_show_errors_without_lock_file(tester: CommandTester) -> None: system_pyproject_file = SelfCommand.get_default_system_pyproject_file() system_pyproject_file.write_text( tomlkit.dumps( { "tool": { "poetry": { "name": "poetry-instance", "version": __version__, "dependencies": {"python": "^3.9", "poetry": __version__}, } } } ), encoding="utf-8", ) system_pyproject_file.parent.joinpath("poetry.lock").unlink(missing_ok=True) assert tester.execute() == 1 assert ( tester.io.fetch_error() == "Error: poetry.lock not found. Run `poetry self lock` to create it.\n" ) ================================================ FILE: tests/console/commands/self/test_show_plugins.py ================================================ from __future__ import annotations from importlib import metadata from typing import TYPE_CHECKING from typing import Any import pytest from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin import Plugin if TYPE_CHECKING: from collections.abc import Callable from os import PathLike from pathlib import Path from cleo.io.io import IO from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.plugins.base_plugin import BasePlugin from poetry.poetry import Poetry from poetry.repositories import Repository from poetry.utils.env import Env from tests.helpers import PoetryTestApplication from tests.types import CommandTesterFactory class DoNothingPlugin(Plugin): def activate(self, poetry: Poetry, io: IO) -> None: pass class EntryPoint(metadata.EntryPoint): def load(self) -> type[BasePlugin]: if self.group == ApplicationPlugin.group: return ApplicationPlugin return DoNothingPlugin @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("self show plugins") @pytest.fixture() def plugin_package_requires_dist() -> list[str]: return [] @pytest.fixture() def plugin_package(plugin_package_requires_dist: list[str]) -> Package: package = Package("poetry-plugin", "1.2.3") for requirement in plugin_package_requires_dist: package.add_dependency(Dependency.create_from_pep_508(requirement)) return package @pytest.fixture() def plugin_distro(plugin_package: Package, tmp_path: Path) -> metadata.Distribution: class MockDistribution(metadata.Distribution): def read_text(self, filename: str) -> str | None: if filename == "METADATA": return "\n".join( [ f"Name: {plugin_package.name}", f"Version: {plugin_package.version}", *[ f"Requires-Dist: {dep.to_pep_508()}" for dep in plugin_package.requires ], ] ) return None def locate_file(self, path: str | PathLike[str]) -> Path: return tmp_path / path return MockDistribution() # type: ignore[no-untyped-call] @pytest.fixture def entry_point_name() -> str: return "poetry-plugin" @pytest.fixture def entry_point_values_by_group() -> dict[str, list[str]]: return {} @pytest.fixture def entry_points( entry_point_name: str, entry_point_values_by_group: dict[str, list[str]], plugin_distro: metadata.Distribution, ) -> Callable[..., list[metadata.EntryPoint]]: by_group = { key: [ EntryPoint( # type: ignore[no-untyped-call] name=entry_point_name, group=key, value=value, )._for( # type: ignore[attr-defined] plugin_distro ) for value in values ] for key, values in entry_point_values_by_group.items() } def _entry_points(**params: Any) -> list[metadata.EntryPoint]: group = params.get("group") if group not in by_group: return [] eps: list[metadata.EntryPoint] = by_group[group] return eps return _entry_points @pytest.fixture(autouse=True) def mock_metadata_entry_points( plugin_package: Package, plugin_distro: metadata.Distribution, installed: Repository, mocker: MockerFixture, tmp_venv: Env, entry_points: Callable[..., metadata.EntryPoint], ) -> None: installed.add_package(plugin_package) mocker.patch.object( tmp_venv.site_packages, "find_distribution", return_value=plugin_distro ) mocker.patch.object(metadata, "entry_points", entry_points) @pytest.mark.parametrize("entry_point_name", ["poetry-plugin", "not-package-name"]) @pytest.mark.parametrize( "entry_point_values_by_group", [ { ApplicationPlugin.group: ["FirstApplicationPlugin"], Plugin.group: ["FirstPlugin"], } ], ) def test_show_displays_installed_plugins( app: PoetryTestApplication, tester: CommandTester, ) -> None: tester.execute("") expected = """ - poetry-plugin (1.2.3) 1 plugin and 1 application plugin """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize( "entry_point_values_by_group", [ { ApplicationPlugin.group: [ "FirstApplicationPlugin", "SecondApplicationPlugin", ], Plugin.group: ["FirstPlugin", "SecondPlugin"], } ], ) def test_show_displays_installed_plugins_with_multiple_plugins( app: PoetryTestApplication, tester: CommandTester, ) -> None: tester.execute("") expected = """ - poetry-plugin (1.2.3) 2 plugins and 2 application plugins """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize( "plugin_package_requires_dist", [["foo (>=1.2.3)", "bar (<4.5.6)"]] ) @pytest.mark.parametrize( "entry_point_values_by_group", [ { ApplicationPlugin.group: ["FirstApplicationPlugin"], Plugin.group: ["FirstPlugin"], } ], ) def test_show_displays_installed_plugins_with_dependencies( app: PoetryTestApplication, tester: CommandTester, ) -> None: tester.execute("") expected = """ - poetry-plugin (1.2.3) 1 plugin and 1 application plugin Dependencies - foo (>=1.2.3) - bar (<4.5.6) """ assert tester.io.fetch_output() == expected ================================================ FILE: tests/console/commands/self/test_sync.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.exceptions import CleoNoSuchOptionError from poetry.console.commands.self.sync import SelfSyncCommand # import all tests from the self install command # and run them for sync by overriding the command fixture from tests.console.commands.self.test_install import * # noqa: F403 if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture @pytest.fixture # type: ignore[no-redef] def command() -> str: return "self sync" @pytest.mark.skip("Only relevant for `poetry self install`") # type: ignore[no-redef] def test_sync_deprecation() -> None: """The only test from the self install command that does not work for self sync.""" def test_sync_option_not_available(tester: CommandTester) -> None: with pytest.raises(CleoNoSuchOptionError): tester.execute("--sync") def test_synced_installer(tester: CommandTester, mocker: MockerFixture) -> None: assert isinstance(tester.command, SelfSyncCommand) mock = mocker.patch( "poetry.console.commands.install.InstallCommand.installer", new_callable=mocker.PropertyMock, ) tester.execute() mock.return_value.requires_synchronization.assert_called_with(True) ================================================ FILE: tests/console/commands/self/test_update.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.core.constraints.version import Version from poetry.core.packages.package import Package from poetry.__version__ import __version__ from poetry.factory import Factory from poetry.installation.executor import Executor from poetry.installation.wheel_installer import WheelInstaller if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from tests.helpers import TestRepository from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter @pytest.fixture def setup(mocker: MockerFixture, fixture_dir: FixtureDirGetter) -> None: mocker.patch.object( Executor, "_download", return_value=fixture_dir("distributions").joinpath( "demo-0.1.2-py2.py3-none-any.whl" ), ) mocker.patch.object(WheelInstaller, "install") @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("self update") def test_self_update_can_update_from_recommended_installation( tester: CommandTester, repo: TestRepository, installed: TestRepository, ) -> None: new_version = Version.parse(__version__).next_minor().text old_poetry = Package("poetry", __version__) old_poetry.add_dependency(Factory.create_dependency("cleo", "^0.8.2")) new_poetry = Package("poetry", new_version) new_poetry.add_dependency(Factory.create_dependency("cleo", "^1.0.0")) installed.add_package(old_poetry) installed.add_package(Package("cleo", "0.8.2")) repo.add_package(new_poetry) repo.add_package(Package("cleo", "1.0.0")) tester.execute() expected_output = f"""\ Updating Poetry version ... Using version ^{new_version} for poetry Updating dependencies Resolving dependencies... Package operations: 0 installs, 2 updates, 0 removals - Updating cleo (0.8.2 -> 1.0.0) - Updating poetry ({__version__} -> {new_version}) Writing lock file """ assert tester.io.fetch_output() == expected_output ================================================ FILE: tests/console/commands/self/utils.py ================================================ from __future__ import annotations from pathlib import Path from typing import Any from tomlkit.items import Array from poetry.factory import Factory def get_self_command_dependencies(locked: bool = True) -> Array | None: from poetry.console.commands.self.self_command import SelfCommand from poetry.locations import CONFIG_DIR system_pyproject_file = SelfCommand.get_default_system_pyproject_file() assert system_pyproject_file.exists() assert system_pyproject_file.parent == Path(CONFIG_DIR) if locked: assert system_pyproject_file.parent.joinpath("poetry.lock").exists() poetry = Factory().create_poetry(system_pyproject_file.parent, disable_plugins=True) pyproject: dict[str, Any] = poetry.file.read() content = pyproject.get("dependency-groups", {}) if SelfCommand.ADDITIONAL_PACKAGE_GROUP not in content: return None dependencies = content[SelfCommand.ADDITIONAL_PACKAGE_GROUP] assert isinstance(dependencies, Array) return dependencies ================================================ FILE: tests/console/commands/source/__init__.py ================================================ ================================================ FILE: tests/console/commands/source/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.config.source import Source from poetry.repositories.repository_pool import Priority if TYPE_CHECKING: from poetry.poetry import Poetry from tests.types import CommandTesterFactory from tests.types import ProjectFactory @pytest.fixture def source_one() -> Source: return Source(name="one", url="https://one.com") @pytest.fixture def source_two() -> Source: return Source(name="two", url="https://two.com") @pytest.fixture def source_primary() -> Source: return Source(name="primary", url="https://primary.com", priority=Priority.PRIMARY) @pytest.fixture def source_supplemental() -> Source: return Source( name="supplemental", url="https://supplemental.com", priority=Priority.SUPPLEMENTAL, ) @pytest.fixture def source_explicit() -> Source: return Source( name="explicit", url="https://explicit.com", priority=Priority.EXPLICIT ) @pytest.fixture def source_pypi() -> Source: return Source(name="PyPI") @pytest.fixture def source_pypi_explicit() -> Source: return Source(name="PyPI", priority=Priority.EXPLICIT) _existing_source = Source(name="existing", url="https://existing.com") @pytest.fixture def source_existing() -> Source: return _existing_source PYPROJECT_WITHOUT_POETRY_SECTION = """ [project] name = "source-command-test" version = "0.1.0" """ PYPROJECT_WITHOUT_SOURCES = """ [tool.poetry] name = "source-command-test" version = "0.1.0" description = "" authors = ["Poetry Tester "] [tool.poetry.dependencies] python = "^3.9" [tool.poetry.dev-dependencies] """ PYPROJECT_WITH_SOURCES = f"""{PYPROJECT_WITHOUT_SOURCES} [[tool.poetry.source]] name = "{_existing_source.name}" url = "{_existing_source.url}" """ PYPROJECT_WITH_PYPI = f"""{PYPROJECT_WITHOUT_SOURCES} [[tool.poetry.source]] name = "PyPI" """ PYPROJECT_WITH_PYPI_AND_OTHER = f"""{PYPROJECT_WITH_SOURCES} [[tool.poetry.source]] name = "PyPI" """ @pytest.fixture def poetry_without_poetry_section(project_factory: ProjectFactory) -> Poetry: return project_factory(pyproject_content=PYPROJECT_WITHOUT_POETRY_SECTION) @pytest.fixture def poetry_without_source(project_factory: ProjectFactory) -> Poetry: return project_factory(pyproject_content=PYPROJECT_WITHOUT_SOURCES) @pytest.fixture def poetry_with_source(project_factory: ProjectFactory) -> Poetry: return project_factory(pyproject_content=PYPROJECT_WITH_SOURCES) @pytest.fixture def poetry_with_pypi(project_factory: ProjectFactory) -> Poetry: return project_factory(pyproject_content=PYPROJECT_WITH_PYPI) @pytest.fixture def poetry_with_pypi_and_other(project_factory: ProjectFactory) -> Poetry: return project_factory(pyproject_content=PYPROJECT_WITH_PYPI_AND_OTHER) @pytest.fixture def add_multiple_sources( command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry, source_one: Source, source_two: Source, ) -> None: add = command_tester_factory("source add", poetry=poetry_with_source) for source in [source_one, source_two]: add.execute(f"{source.name} {source.url}") @pytest.fixture def add_all_source_types( command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry, source_primary: Source, source_supplemental: Source, source_explicit: Source, ) -> None: add = command_tester_factory("source add", poetry=poetry_with_source) for source in [ source_primary, source_supplemental, source_explicit, ]: add.execute(f"{source.name} {source.url} --priority={source.name}") ================================================ FILE: tests/console/commands/source/test_add.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.config.source import Source from poetry.repositories.repository_pool import Priority if TYPE_CHECKING: from collections.abc import Iterable from cleo.testers.command_tester import CommandTester from poetry.poetry import Poetry from tests.types import CommandTesterFactory @pytest.fixture def tester( command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry ) -> CommandTester: return command_tester_factory("source add", poetry=poetry_with_source) def assert_source_added( tester: CommandTester, poetry: Poetry, added_source: Source, existing_sources: Iterable[Source] = (), ) -> None: expected_error = "" if tester.io.input.option("priority") is None: expected_error = f"The default priority will change to supplemental in a future release.\n{expected_error}" assert tester.io.fetch_error().strip() == expected_error.strip() expected_output = f"Adding source with name {added_source.name}." assert tester.io.fetch_output().strip() == expected_output poetry.pyproject.reload() sources = poetry.get_sources() assert sources == [*existing_sources, added_source] assert tester.status_code == 0 def test_source_add_simple( tester: CommandTester, source_existing: Source, source_one: Source, poetry_with_source: Poetry, ) -> None: tester.execute(f"{source_one.name} {source_one.url}") assert_source_added(tester, poetry_with_source, source_one, [source_existing]) def test_source_add_simple_without_existing_sources( tester: CommandTester, source_one: Source, poetry_without_source: Poetry, ) -> None: tester.execute(f"{source_one.name} {source_one.url}") assert_source_added(tester, poetry_without_source, source_one) def test_source_add_simple_without_existing_poetry_section( tester: CommandTester, source_one: Source, poetry_without_poetry_section: Poetry, ) -> None: tester.execute(f"{source_one.name} {source_one.url}") assert_source_added(tester, poetry_without_poetry_section, source_one) def test_source_add_supplemental( tester: CommandTester, source_existing: Source, source_supplemental: Source, poetry_with_source: Poetry, ) -> None: tester.execute( f"--priority=supplemental {source_supplemental.name} {source_supplemental.url}" ) assert_source_added( tester, poetry_with_source, source_supplemental, [source_existing] ) def test_source_add_explicit( tester: CommandTester, source_existing: Source, source_explicit: Source, poetry_with_source: Poetry, ) -> None: tester.execute(f"--priority=explicit {source_explicit.name} {source_explicit.url}") assert_source_added(tester, poetry_with_source, source_explicit, [source_existing]) def test_source_add_error_no_url(tester: CommandTester) -> None: tester.execute("foo") assert ( tester.io.fetch_error().strip() == "A custom source cannot be added without a URL." ) assert tester.status_code == 1 def test_source_add_error_pypi(tester: CommandTester) -> None: tester.execute("pypi https://test.pypi.org/simple/") assert ( tester.io.fetch_error().strip() == "The URL of PyPI is fixed and cannot be set." ) assert tester.status_code == 1 @pytest.mark.parametrize("name", ["pypi", "PyPI"]) def test_source_add_pypi( name: str, tester: CommandTester, source_existing: Source, source_pypi: Source, poetry_with_source: Poetry, ) -> None: tester.execute(name) assert_source_added(tester, poetry_with_source, source_pypi, [source_existing]) def test_source_add_pypi_explicit( tester: CommandTester, source_existing: Source, source_pypi_explicit: Source, poetry_with_source: Poetry, ) -> None: tester.execute("--priority=explicit PyPI") assert_source_added( tester, poetry_with_source, source_pypi_explicit, [source_existing] ) @pytest.mark.parametrize("modifier", ["lower", "upper"]) def test_source_add_existing_no_change_except_case_of_name( modifier: str, tester: CommandTester, source_existing: Source, poetry_with_source: Poetry, ) -> None: name = getattr(source_existing.name, modifier)() tester.execute(f"--priority=primary {name} {source_existing.url}") assert ( tester.io.fetch_output().strip() == f"Source with name {name} already exists. Updating." ) poetry_with_source.pyproject.reload() sources = poetry_with_source.get_sources() assert len(sources) == 1 assert sources[0].name == getattr(source_existing.name, modifier)() assert sources[0].url == source_existing.url assert sources[0].priority == source_existing.priority @pytest.mark.parametrize("modifier", ["lower", "upper"]) def test_source_add_existing_updating( modifier: str, tester: CommandTester, source_existing: Source, poetry_with_source: Poetry, ) -> None: name = getattr(source_existing.name, modifier)() tester.execute(f"--priority=supplemental {name} {source_existing.url}") assert ( tester.io.fetch_output().strip() == f"Source with name {name} already exists. Updating." ) poetry_with_source.pyproject.reload() sources = poetry_with_source.get_sources() assert len(sources) == 1 assert sources[0] != source_existing expected_source = Source( name=name, url=source_existing.url, priority=Priority.SUPPLEMENTAL ) assert sources[0] == expected_source ================================================ FILE: tests/console/commands/source/test_remove.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from poetry.config.source import Source from poetry.poetry import Poetry from tests.types import CommandTesterFactory @pytest.fixture def tester( command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry, add_multiple_sources: None, ) -> CommandTester: return command_tester_factory("source remove", poetry=poetry_with_source) @pytest.fixture def tester_pypi( command_tester_factory: CommandTesterFactory, poetry_with_pypi: Poetry, ) -> CommandTester: return command_tester_factory("source remove", poetry=poetry_with_pypi) @pytest.fixture def tester_pypi_and_other( command_tester_factory: CommandTesterFactory, poetry_with_pypi_and_other: Poetry, ) -> CommandTester: return command_tester_factory("source remove", poetry=poetry_with_pypi_and_other) @pytest.mark.parametrize("modifier", ["lower", "upper"]) def test_source_remove_simple( tester: CommandTester, poetry_with_source: Poetry, source_existing: Source, source_one: Source, source_two: Source, modifier: str, ) -> None: tester.execute(getattr(f"{source_existing.name}", modifier)()) assert ( tester.io.fetch_output().strip() == f"Removing source with name {source_existing.name}." ) poetry_with_source.pyproject.reload() sources = poetry_with_source.get_sources() assert sources == [source_one, source_two] assert tester.status_code == 0 @pytest.mark.parametrize("name", ["pypi", "PyPI"]) def test_source_remove_pypi( name: str, tester_pypi: CommandTester, poetry_with_pypi: Poetry ) -> None: tester_pypi.execute(name) assert tester_pypi.io.fetch_output().strip() == "Removing source with name PyPI." poetry_with_pypi.pyproject.reload() sources = poetry_with_pypi.get_sources() assert sources == [] assert tester_pypi.status_code == 0 @pytest.mark.parametrize("name", ["pypi", "PyPI"]) def test_source_remove_pypi_and_other( name: str, tester_pypi_and_other: CommandTester, poetry_with_pypi_and_other: Poetry, source_existing: Source, ) -> None: tester_pypi_and_other.execute(name) assert ( tester_pypi_and_other.io.fetch_output().strip() == "Removing source with name PyPI." ) poetry_with_pypi_and_other.pyproject.reload() sources = poetry_with_pypi_and_other.get_sources() assert sources == [source_existing] assert tester_pypi_and_other.status_code == 0 @pytest.mark.parametrize("name", ["foo", "pypi", "PyPI"]) def test_source_remove_error(name: str, tester: CommandTester) -> None: tester.execute(name) assert tester.io.fetch_error().strip() == f"Source with name {name} was not found." assert tester.status_code == 1 ================================================ FILE: tests/console/commands/source/test_show.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.repositories.pypi_repository import PyPiRepository if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from poetry.config.source import Source from poetry.poetry import Poetry from tests.types import CommandTesterFactory @pytest.fixture def tester( command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry, add_multiple_sources: None, ) -> CommandTester: return command_tester_factory("source show", poetry=poetry_with_source) @pytest.fixture def tester_no_sources( command_tester_factory: CommandTesterFactory, poetry_without_source: Poetry, ) -> CommandTester: return command_tester_factory("source show", poetry=poetry_without_source) @pytest.fixture def tester_pypi( command_tester_factory: CommandTesterFactory, poetry_with_pypi: Poetry, ) -> CommandTester: return command_tester_factory("source show", poetry=poetry_with_pypi) @pytest.fixture def tester_pypi_and_other( command_tester_factory: CommandTesterFactory, poetry_with_pypi_and_other: Poetry, ) -> CommandTester: return command_tester_factory("source show", poetry=poetry_with_pypi_and_other) @pytest.fixture def tester_all_types( command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry, add_all_source_types: None, ) -> CommandTester: return command_tester_factory("source show", poetry=poetry_with_source) def test_source_show_simple(tester: CommandTester) -> None: tester.execute("") expected = """\ name : existing url : https://existing.com priority : primary name : one url : https://one.com priority : primary name : two url : https://two.com priority : primary """.splitlines() assert [ line.strip() for line in tester.io.fetch_output().strip().splitlines() ] == expected assert tester.status_code == 0 @pytest.mark.parametrize("modifier", ["lower", "upper"]) def test_source_show_one( tester: CommandTester, source_one: Source, modifier: str ) -> None: tester.execute(getattr(f"{source_one.name}", modifier)()) expected = """\ name : one url : https://one.com priority : primary """.splitlines() assert [ line.strip() for line in tester.io.fetch_output().strip().splitlines() ] == expected assert tester.status_code == 0 @pytest.mark.parametrize("modifier", ["lower", "upper"]) def test_source_show_two( tester: CommandTester, source_one: Source, source_two: Source, modifier: str ) -> None: tester.execute(getattr(f"{source_one.name} {source_two.name}", modifier)()) expected = """\ name : one url : https://one.com priority : primary name : two url : https://two.com priority : primary """.splitlines() assert [ line.strip() for line in tester.io.fetch_output().strip().splitlines() ] == expected assert tester.status_code == 0 @pytest.mark.parametrize( "source_str", ( "source_primary", "source_supplemental", "source_explicit", ), ) def test_source_show_given_priority( tester_all_types: CommandTester, source_str: str, request: pytest.FixtureRequest ) -> None: source = request.getfixturevalue(source_str) tester_all_types.execute(f"{source.name}") expected = f"""\ name : {source.name} url : {source.url} priority : {source.name} """.splitlines() assert [ line.strip() for line in tester_all_types.io.fetch_output().strip().splitlines() ] == expected assert tester_all_types.status_code == 0 def test_source_show_pypi(tester_pypi: CommandTester) -> None: tester_pypi.execute("") expected = """\ name : PyPI priority : primary """.splitlines() assert [ line.strip() for line in tester_pypi.io.fetch_output().strip().splitlines() ] == expected assert tester_pypi.status_code == 0 def test_source_show_pypi_and_other(tester_pypi_and_other: CommandTester) -> None: tester_pypi_and_other.execute("") expected = """\ name : existing url : https://existing.com priority : primary name : PyPI priority : primary """.splitlines() assert [ line.strip() for line in tester_pypi_and_other.io.fetch_output().strip().splitlines() ] == expected assert tester_pypi_and_other.status_code == 0 def test_source_show_no_sources(tester_no_sources: CommandTester) -> None: tester_no_sources.execute("error") assert ( tester_no_sources.io.fetch_output().strip() == "No sources configured for this project." ) assert tester_no_sources.status_code == 0 def test_source_show_no_sources_implicit_pypi( tester_no_sources: CommandTester, poetry_without_source: Poetry ) -> None: poetry_without_source.pool.add_repository(PyPiRepository()) tester_no_sources.execute("") output = tester_no_sources.io.fetch_output().strip() assert "No sources configured for this project." in output assert "PyPI is implicitly enabled as a primary source." in output assert tester_no_sources.status_code == 0 def test_source_show_error(tester: CommandTester) -> None: tester.execute("error") assert tester.io.fetch_error().strip() == "No source found with name(s): error" assert tester.status_code == 1 ================================================ FILE: tests/console/commands/test_about.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from tests.types import CommandTesterFactory @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("about") def test_about(tester: CommandTester) -> None: from importlib import metadata tester.execute() expected = f"""\ Poetry - Package Management for Python Version: {metadata.version("poetry")} Poetry-Core Version: {metadata.version("poetry-core")} Poetry is a dependency manager tracking local dependencies of your projects and\ libraries. See https://github.com/python-poetry/poetry for more information. """ assert tester.io.fetch_output() == expected ================================================ FILE: tests/console/commands/test_add.py ================================================ from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING from typing import cast import pytest import tomlkit from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.packages.package import Package from poetry.console.commands.installer_command import InstallerCommand from poetry.puzzle.exceptions import SolverProblemError from poetry.repositories.legacy_repository import LegacyRepository from poetry.utils.dependency_specification import RequirementsParser from tests.helpers import TestLocker from tests.helpers import get_dependency from tests.helpers import get_package if TYPE_CHECKING: from typing import Any from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from tomlkit import TOMLDocument from poetry.config.config import Config from poetry.poetry import Poetry from poetry.utils.env import MockEnv from poetry.utils.env import VirtualEnv from tests.helpers import PoetryTestApplication from tests.helpers import TestRepository from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture(autouse=True) def config(config: Config) -> Config: # Disable parallel installs to get reproducible output. config.merge({"installer": {"parallel": False}}) return config @pytest.fixture def poetry_with_up_to_date_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: source = fixture_dir("up_to_date_lock") return project_factory( name="foobar", pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), poetry_lock_content=(source / "poetry.lock").read_text(encoding="utf-8"), ) @pytest.fixture def poetry_with_path_dependency( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: source = fixture_dir("with_path_dependency") poetry = project_factory( name="foobar", source=source, use_test_locker=False, ) return poetry @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("add") @pytest.fixture(autouse=True) def repo_add_default_packages(repo: TestRepository) -> None: msgpack_optional_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6") msgpack = get_package("msgpack-python", "0.5.6") repo.add_package(msgpack) redis_dep = get_dependency("redis", ">=3.3.6 <4.0.0", optional=True) redis = get_package("redis", "3.4.0") repo.add_package(redis) cachy010 = get_package("cachy", "0.1.0") cachy010.extras = {canonicalize_name("msgpack"): [msgpack_dep]} cachy010.add_dependency(msgpack_optional_dep) repo.add_package(cachy010) cachy020 = get_package("cachy", "0.2.0") cachy020.extras = {canonicalize_name("msgpack"): [get_dependency("msgpack-python")]} cachy020.add_dependency(msgpack_dep) repo.add_package(cachy020) cleo065 = get_package("cleo", "0.6.5") cleo065.add_dependency(msgpack_optional_dep) cleo065.add_dependency(redis_dep) cleo065.extras = { canonicalize_name("redis"): [redis_dep], canonicalize_name("msgpack"): [msgpack_dep], } repo.add_package(cleo065) repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("tomlkit", "0.5.5")) repo.add_package(get_package("pyyaml", "3.13")) repo.add_package(get_package("pyyaml", "4.2b2")) repo.add_package(get_package("torch", "2.4.0+cpu")) def test_add_no_constraint(app: PoetryTestApplication, tester: CommandTester) -> None: tester.execute("cachy") expected = """\ Using version ^0.2.0 for cachy Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"] == "^0.2.0" def test_add_local_version(app: PoetryTestApplication, tester: CommandTester) -> None: tester.execute("torch") expected = """\ Using version ^2.4.0 for torch Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing torch (2.4.0+cpu) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 1 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "torch" in content["dependencies"] assert content["dependencies"]["torch"] == "^2.4.0" def test_add_non_package_mode_no_name( project_factory: ProjectFactory, command_tester_factory: CommandTesterFactory, ) -> None: poetry = project_factory( name="foobar", pyproject_content="[tool.poetry]\npackage-mode = false\n" ) tester = command_tester_factory("add", poetry=poetry) tester.execute("cachy") assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = poetry.file.read() content = pyproject["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"] == "^0.2.0" def test_add_replace_by_constraint( app: PoetryTestApplication, tester: CommandTester ) -> None: tester.execute("cachy") expected = """\ Using version ^0.2.0 for cachy Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"] == "^0.2.0" tester.execute("cachy@0.1.0") expected = """ Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.1.0) Writing lock file """ assert tester.io.fetch_output() == expected pyproject2: dict[str, Any] = app.poetry.file.read() content = pyproject2["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"] == "0.1.0" def test_add_no_constraint_editable_error( app: PoetryTestApplication, tester: CommandTester ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] tester.execute("-e cachy") expected = """ Failed to add packages. Only vcs/path dependencies support editable installs.\ cachy is neither. No changes were applied. """ assert tester.status_code == 1 assert tester.io.fetch_error() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 0 pyproject2: dict[str, Any] = app.poetry.file.read() assert content == pyproject2["tool"]["poetry"] def test_add_equal_constraint(tester: CommandTester) -> None: tester.execute("cachy==0.1.0") expected = """\ Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.1.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 1 def test_add_greater_constraint(tester: CommandTester) -> None: tester.execute("cachy>=0.1.0") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 @pytest.mark.parametrize("extra_name", ["msgpack", "MsgPack"]) def test_add_constraint_with_extras( tester: CommandTester, extra_name: str, ) -> None: tester.execute(f"cachy[{extra_name}]>=0.1.0,<0.2.0") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.1.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 def test_add_constraint_dependencies(tester: CommandTester) -> None: tester.execute("cachy=0.2.0") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 def test_add_with_markers(app: PoetryTestApplication, tester: CommandTester) -> None: marker = "python_version <= '3.4' or sys_platform == 'win32'" tester.execute(f"""cachy --markers "{marker}" """) pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"]["version"] == "^0.2.0" assert content["dependencies"]["cachy"]["markers"] == marker def test_add_git_constraint( app: PoetryTestApplication, tester: CommandTester, tmp_venv: VirtualEnv, ) -> None: assert isinstance(tester.command, InstallerCommand) tester.command.set_env(tmp_venv) tester.execute("git+https://github.com/demo/demo.git") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) Writing lock file """ assert tester.io.fetch_output() == expected assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "demo" in content["dependencies"] assert content["dependencies"]["demo"] == { "git": "https://github.com/demo/demo.git" } def test_add_git_constraint_with_poetry( tester: CommandTester, tmp_venv: VirtualEnv, ) -> None: assert isinstance(tester.command, InstallerCommand) tester.command.set_env(tmp_venv) tester.execute("git+https://github.com/demo/pyproject-demo.git") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) Writing lock file """ assert tester.io.fetch_output() == expected assert tester.command.installer.executor.installations_count == 2 @pytest.mark.parametrize("extra_name", ["foo", "FOO"]) def test_add_git_constraint_with_extras( app: PoetryTestApplication, tester: CommandTester, tmp_venv: VirtualEnv, extra_name: str, ) -> None: assert isinstance(tester.command, InstallerCommand) tester.command.set_env(tmp_venv) tester.execute(f"git+https://github.com/demo/demo.git[{extra_name},bar]") expected = """\ Updating dependencies Resolving dependencies... Package operations: 4 installs, 0 updates, 0 removals - Installing cleo (0.6.5) - Installing pendulum (1.4.4) - Installing tomlkit (0.5.5) - Installing demo (0.1.2 9cf87a2) Writing lock file """ assert tester.io.fetch_output().strip() == expected.strip() assert tester.command.installer.executor.installations_count == 4 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "demo" in content["dependencies"] assert content["dependencies"]["demo"] == { "git": "https://github.com/demo/demo.git", "extras": [extra_name, "bar"], } @pytest.mark.parametrize( "url, rev", [ ("git+https://github.com/demo/subdirectories.git#subdirectory=two", None), ( "git+https://github.com/demo/subdirectories.git@master#subdirectory=two", "master", ), ], ) def test_add_git_constraint_with_subdirectory( url: str, rev: str | None, app: PoetryTestApplication, tester: CommandTester, ) -> None: tester.execute(url) expected = """\ Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing two (2.0.0 9cf87a2) Writing lock file """ assert tester.io.fetch_output().strip() == expected.strip() assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 1 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] constraint = { "git": "https://github.com/demo/subdirectories.git", "subdirectory": "two", } if rev: constraint["rev"] = rev assert "two" in content["dependencies"] assert content["dependencies"]["two"] == constraint @pytest.mark.parametrize("editable", [False, True]) def test_add_git_ssh_constraint( editable: bool, app: PoetryTestApplication, tester: CommandTester, tmp_venv: VirtualEnv, ) -> None: assert isinstance(tester.command, InstallerCommand) tester.command.set_env(tmp_venv) url = "git+ssh://git@github.com/demo/demo.git@develop" tester.execute(f"{url}" if not editable else f"-e {url}") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) Writing lock file """ assert tester.io.fetch_output() == expected assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "demo" in content["dependencies"] expected_content: dict[str, Any] = { "git": "ssh://git@github.com/demo/demo.git", "rev": "develop", } if editable: expected_content["develop"] = True assert content["dependencies"]["demo"] == expected_content @pytest.mark.parametrize( "required_fixtures", [["git/github.com/demo/demo"]], ) @pytest.mark.parametrize("editable", [False, True]) def test_add_directory_constraint( editable: bool, app: PoetryTestApplication, tester: CommandTester, ) -> None: path = "../git/github.com/demo/demo" tester.execute(f"{path}" if not editable else f"-e {path}") demo_path = app.poetry.file.path.parent.joinpath(path).resolve().as_posix() expected = f"""\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 {demo_path}) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "demo" in content["dependencies"] expected_content: dict[str, Any] = {"path": path} if editable: expected_content["develop"] = True assert content["dependencies"]["demo"] == expected_content def test_add_to_new_group_keeps_existing_group( app: PoetryTestApplication, tester: CommandTester ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] example = [ "cachy (>=0.2.0,<0.3.0)", ] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) tester.execute("-G foo pendulum") expected = """\ Using version ^1.4.4 for pendulum """ assert tester.io.fetch_output().startswith(expected) assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 1 pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) assert "example" in pyproject["dependency-groups"] assert pyproject["dependency-groups"]["example"] == [ "cachy (>=0.2.0,<0.3.0)", ] assert pyproject["dependency-groups"]["foo"] == [ "pendulum (>=1.4.4,<2.0.0)", ] @pytest.mark.parametrize("additional_poetry_group", [False, True]) def test_add_to_existing_group( app: PoetryTestApplication, tester: CommandTester, additional_poetry_group: bool ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] example = [ "cachy (>=0.2.0,<0.3.0)", {include-group = "included"}, ] included = [] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] if additional_poetry_group: poetry_groups_content: dict[str, Any] = tomlkit.parse( """\ [tool.poetry.group.example.dependencies] cachy = { allow-prereleases = true } """ ) pyproject["tool"]["poetry"]["group"] = poetry_groups_content["tool"]["poetry"][ "group" ] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) tester.execute("-G example pendulum") expected = """\ Using version ^1.4.4 for pendulum """ assert tester.io.fetch_output().startswith(expected) assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 1 pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) assert "example" in pyproject["dependency-groups"] assert pyproject["dependency-groups"]["example"] == [ "cachy (>=0.2.0,<0.3.0)", {"include-group": "included"}, "pendulum (>=1.4.4,<2.0.0)", ] if additional_poetry_group: assert "example" in pyproject["tool"]["poetry"]["group"] assert pyproject["tool"]["poetry"]["group"]["example"]["dependencies"] == { "cachy": {"allow-prereleases": True}, } else: assert "group" not in pyproject["tool"]["poetry"] def test_add_to_group_with_latest_overwrite_existing( app: PoetryTestApplication, tester: CommandTester ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] example = [ {include-group = "included"}, "cachy (>=0.1.0,<0.2.0)", "pendulum (>=1.4.4,<2.0.0)", ] included = [] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) tester.execute("-G example cachy@latest") expected = """\ Using version ^0.2.0 for cachy """ assert tester.io.fetch_output().startswith(expected) assert isinstance(tester.command, InstallerCommand) # `cachy` has `msgpack-python` as dependency. So installation count increases by 1. assert tester.command.installer.executor.installations_count == 2 pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) assert "example" in pyproject["dependency-groups"] assert pyproject["dependency-groups"]["example"] == [ {"include-group": "included"}, "cachy (>=0.2.0,<0.3.0)", "pendulum (>=1.4.4,<2.0.0)", ] def test_add_multiple_dependencies_to_dependency_group( app: PoetryTestApplication, tester: CommandTester ) -> None: tester.execute("-G example cachy pendulum") expected = """\ Using version ^0.2.0 for cachy Using version ^1.4.4 for pendulum """ assert tester.io.fetch_output().startswith(expected) pyproject: dict[str, Any] = app.poetry.file.read() assert isinstance(tester.command, InstallerCommand) # `cachy` has `msgpack-python` as dependency. So installation count increases by 1. assert tester.command.installer.executor.installations_count == 3 assert "example" in pyproject["dependency-groups"] assert pyproject["dependency-groups"]["example"] == [ "cachy (>=0.2.0,<0.3.0)", "pendulum (>=1.4.4,<2.0.0)", ] def test_add_to_group_uses_existing_legacy_group( app: PoetryTestApplication, tester: CommandTester ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() groups_content: dict[str, Any] = tomlkit.parse( """\ [tool.poetry.group.example.dependencies] pendulum = "^1.4.4" """ ) pyproject["tool"]["poetry"]["group"] = groups_content["tool"]["poetry"]["group"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) tester.execute("-G example cachy") expected = """\ Using version ^0.2.0 for cachy """ assert tester.io.fetch_output().startswith(expected) assert isinstance(tester.command, InstallerCommand) # `cachy` has `msgpack-python` as dependency. So installation count increases by 1. assert tester.command.installer.executor.installations_count == 2 pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) assert "dependency-groups" not in pyproject assert "example" in pyproject["tool"]["poetry"]["group"] assert "pendulum" in pyproject["tool"]["poetry"]["group"]["example"]["dependencies"] assert "cachy" in pyproject["tool"]["poetry"]["group"]["example"]["dependencies"] @pytest.mark.parametrize( "required_fixtures", [["git/github.com/demo/demo"]], ) def test_add_group_directory_constraint_mix_pep735( app: PoetryTestApplication, tester: CommandTester, ) -> None: path = "../git/github.com/demo/demo" tester.execute(f"-G example -e {path}") demo_path = app.poetry.file.path.parent.joinpath(path).resolve().as_posix() expected = f"""\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 {demo_path}) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() assert "demo @ file://" in pyproject["dependency-groups"]["example"][0] assert demo_path in pyproject["dependency-groups"]["example"][0] assert "demo" in pyproject["tool"]["poetry"]["group"]["example"]["dependencies"] assert pyproject["tool"]["poetry"]["group"]["example"]["dependencies"]["demo"] == { "develop": True } @pytest.mark.parametrize( "required_fixtures", [["git/github.com/demo/pyproject-demo"]], ) def test_add_directory_with_poetry( app: PoetryTestApplication, tester: CommandTester, ) -> None: path = "../git/github.com/demo/pyproject-demo" tester.execute(f"{path}") demo_path = app.poetry.file.path.parent.joinpath(path).resolve().as_posix() expected = f"""\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 {demo_path}) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 @pytest.mark.parametrize( "required_fixtures", [["distributions/demo-0.1.0-py2.py3-none-any.whl"]], ) def test_add_file_constraint_wheel( app: PoetryTestApplication, tester: CommandTester, ) -> None: path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" tester.execute(f"{path}") demo_path = app.poetry.file.path.parent.joinpath(path).resolve().as_posix() expected = f"""\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 {demo_path}) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "demo" in content["dependencies"] assert content["dependencies"]["demo"] == {"path": path} @pytest.mark.parametrize( "required_fixtures", [["distributions/demo-0.1.0.tar.gz"]], ) def test_add_file_constraint_sdist( app: PoetryTestApplication, tester: CommandTester, ) -> None: path = "../distributions/demo-0.1.0.tar.gz" tester.execute(f"{path}") demo_path = app.poetry.file.path.parent.joinpath(path).resolve().as_posix() expected = f"""\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 {demo_path}) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "demo" in content["dependencies"] assert content["dependencies"]["demo"] == {"path": path} @pytest.mark.parametrize("extra_name", ["msgpack", "MsgPack"]) def test_add_constraint_with_extras_option( app: PoetryTestApplication, tester: CommandTester, extra_name: str, ) -> None: tester.execute(f"cachy=0.2.0 --extras {extra_name}") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"] == { "version": "0.2.0", "extras": [extra_name], } def test_add_url_constraint_wheel( app: PoetryTestApplication, tester: CommandTester, mocker: MockerFixture, ) -> None: p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." tester.execute( "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" ) expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo\ (0.1.0 https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "demo" in content["dependencies"] assert content["dependencies"]["demo"] == { "url": "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" } @pytest.mark.parametrize("extra_name", ["foo", "FOO"]) def test_add_url_constraint_wheel_with_extras( app: PoetryTestApplication, tester: CommandTester, extra_name: str, ) -> None: tester.execute( "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" f"[{extra_name},bar]" ) expected = """\ Updating dependencies Resolving dependencies... Package operations: 4 installs, 0 updates, 0 removals - Installing cleo (0.6.5) - Installing pendulum (1.4.4) - Installing tomlkit (0.5.5) - Installing demo\ (0.1.0 https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl) Writing lock file """ # Order might be different, split into lines and compare the overall output. expected_lines = set(expected.splitlines()) output = set(tester.io.fetch_output().splitlines()) assert output == expected_lines assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 4 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "demo" in content["dependencies"] assert content["dependencies"]["demo"] == { "url": ( "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" ), "extras": [extra_name, "bar"], } @pytest.mark.parametrize("project_dependencies", [True, False]) @pytest.mark.parametrize( ("existing_extras", "expected_extras"), [ (None, {"my-extra": ["cachy (==0.2.0)"]}), ( {"other": ["tomlkit (<2)"]}, {"other": ["tomlkit (<2)"], "my-extra": ["cachy (==0.2.0)"]}, ), ( {"my-extra": ["tomlkit (<2)"]}, {"my-extra": ["tomlkit (<2)", "cachy (==0.2.0)"]}, ), ( {"my-extra": ["tomlkit (<2)", "cachy (==0.1.0)", "pendulum (>1)"]}, {"my-extra": ["tomlkit (<2)", "cachy (==0.2.0)", "pendulum (>1)"]}, ), ], ) def test_add_constraint_with_optional( app: PoetryTestApplication, tester: CommandTester, project_dependencies: bool, existing_extras: dict[str, list[str]] | None, expected_extras: dict[str, list[str]], ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() if project_dependencies: pyproject["project"]["dependencies"] = ["tomlkit (<1)"] if existing_extras: pyproject["project"]["optional-dependencies"] = existing_extras else: pyproject["tool"]["poetry"]["dependencies"]["tomlkit"] = "<1" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.reset_poetry() tester.execute("cachy=0.2.0 --optional my-extra") assert tester.io.fetch_output().endswith("Writing lock file\n") assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count > 0 # check pyproject content pyproject2: dict[str, Any] = app.poetry.file.read() project_content = pyproject2["project"] poetry_content = pyproject2["tool"]["poetry"] if project_dependencies: assert "cachy" not in poetry_content["dependencies"] assert "cachy" not in project_content["dependencies"] assert "my-extra" in project_content["optional-dependencies"] assert project_content["optional-dependencies"] == expected_extras assert not tester.io.fetch_error() else: assert "dependencies" not in project_content assert "optional-dependencies" not in project_content assert "cachy" in poetry_content["dependencies"] assert poetry_content["dependencies"]["cachy"] == { "version": "0.2.0", "optional": True, } assert ( "Optional dependencies will not be added to extras in legacy mode." in tester.io.fetch_error() ) # check lock content if project_dependencies: lock_data = app.poetry.locker.lock_data extras = lock_data["extras"] assert list(extras) == sorted(expected_extras) assert extras["my-extra"] == sorted( e.split(" ")[0] for e in expected_extras["my-extra"] ) added_package: dict[str, Any] | None = None for package in lock_data["package"]: if package["name"] == "cachy": added_package = package break assert added_package is not None assert added_package.get("markers") == 'extra == "my-extra"' def test_add_constraint_with_optional_not_main_group( app: PoetryTestApplication, tester: CommandTester ) -> None: with pytest.raises(ValueError) as e: tester.execute("cachy=0.2.0 --group dev --optional my-extra") assert str(e.value) == "You can only add optional dependencies to the main group" def test_add_constraint_with_python( app: PoetryTestApplication, tester: CommandTester ) -> None: tester.execute("cachy=0.2.0 --python >=2.7") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"] == {"version": "0.2.0", "python": ">=2.7"} def test_add_constraint_with_platform( app: PoetryTestApplication, tester: CommandTester, env: MockEnv, ) -> None: platform = sys.platform env._platform = platform tester.execute(f"cachy=0.2.0 --platform {platform} -vvv") expected = """\ Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"] == { "version": "0.2.0", "platform": platform, } def test_add_constraint_with_source( app: PoetryTestApplication, poetry: Poetry, tester: CommandTester, mocker: MockerFixture, ) -> None: repo = LegacyRepository(name="my-index", url="https://my-index.fake") package = Package( "cachy", Version.parse("0.2.0"), source_type="legacy", source_reference=repo.name, source_url=repo._url, yanked=False, ) mocker.patch.object(repo, "package", return_value=package) mocker.patch.object(repo, "_find_packages", wraps=lambda _, name: [package]) poetry.pool.add_repository(repo) tester.execute("cachy=0.2.0 --source my-index") expected = """\ Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 1 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "cachy" in content["dependencies"] assert content["dependencies"]["cachy"] == { "version": "0.2.0", "source": "my-index", } def test_add_constraint_with_source_that_does_not_exist(tester: CommandTester) -> None: with pytest.raises(IndexError) as e: tester.execute("foo --source i-dont-exist") assert str(e.value) == 'Repository "i-dont-exist" does not exist.' def test_add_constraint_not_found_with_source( poetry: Poetry, mocker: MockerFixture, tester: CommandTester, ) -> None: repo = LegacyRepository(name="my-index", url="https://my-index.fake") mocker.patch.object(repo, "find_packages", return_value=[]) poetry.pool.add_repository(repo) pypi = poetry.pool.repositories[0] pypi.add_package(get_package("cachy", "0.2.0")) with pytest.raises(ValueError) as e: tester.execute("cachy --source my-index") assert str(e.value) == "Could not find a matching version of package cachy" @pytest.mark.parametrize("group_name", ["dev", "foo.BAR"]) def test_add_to_section_that_does_not_exist_yet( app: PoetryTestApplication, tester: CommandTester, group_name: str, ) -> None: tester.execute(f"cachy --group {group_name}") expected = """\ Using version ^0.2.0 for cachy Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["dependency-groups"] assert content[group_name][0] == "cachy (>=0.2.0,<0.3.0)" escaped_group_name = f'"{group_name}"' if "." in group_name else group_name expected = f"""\ {escaped_group_name} = [ "cachy (>=0.2.0,<0.3.0)" ] """ string_content = content.as_string() if "\r\n" in string_content: # consistent line endings expected = expected.replace("\n", "\r\n") assert expected in string_content def test_add_creating_poetry_section_does_not_remove_existing_tools( repo: TestRepository, project_factory: ProjectFactory, command_tester_factory: CommandTesterFactory, ) -> None: repo.add_package(get_package("cachy", "0.2.0")) poetry = project_factory( name="foobar", pyproject_content=( '[project]\nname = "foobar"\nversion="0"\n[tool.foo]\nkey = "value"\n' ), ) tester = command_tester_factory("add", poetry=poetry) tester.execute("--group dev cachy") assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = poetry.file.read() content = pyproject["dependency-groups"] assert content["dev"][0] == "cachy (>=0.2.0,<0.3.0)" assert "foo" in pyproject["tool"] assert pyproject["tool"]["foo"]["key"] == "value" def test_add_to_dev_group(app: PoetryTestApplication, tester: CommandTester) -> None: tester.execute("cachy --dev") expected = """\ Using version ^0.2.0 for cachy Updating dependencies Resolving dependencies... Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing cachy (0.2.0) Writing lock file """ assert tester.io.fetch_error() == "" assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["dependency-groups"] assert content["dev"][0] == "cachy (>=0.2.0,<0.3.0)" def test_add_should_not_select_prereleases( app: PoetryTestApplication, tester: CommandTester ) -> None: tester.execute("pyyaml") expected = """\ Using version ^3.13 for pyyaml Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing pyyaml (3.13) Writing lock file """ assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 1 pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "pyyaml" in content["dependencies"] assert content["dependencies"]["pyyaml"] == "^3.13" @pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_skip_when_adding_existing_package_with_no_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester, project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() if project_dependencies: pyproject["project"]["dependencies"] = ["foo>1"] else: pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) repo.add_package(get_package("foo", "1.1.2")) tester.execute("foo") expected = """\ The following packages are already present in the pyproject.toml and will be skipped: - foo If you want to update it to the latest compatible version,\ you can use `poetry update package`. If you prefer to upgrade it to the latest available version,\ you can use `poetry add package@latest`. """ assert expected in tester.io.fetch_output() @pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_skip_when_adding_canonicalized_existing_package_with_no_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester, project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() if project_dependencies: pyproject["project"]["dependencies"] = ["foo-bar>1"] else: pyproject["tool"]["poetry"]["dependencies"]["foo-bar"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) repo.add_package(get_package("foo-bar", "1.1.2")) tester.execute("Foo_Bar") expected = """\ The following packages are already present in the pyproject.toml and will be skipped: - Foo_Bar If you want to update it to the latest compatible version,\ you can use `poetry update package`. If you prefer to upgrade it to the latest available version,\ you can use `poetry add package@latest`. """ assert expected in tester.io.fetch_output() def test_add_should_fail_circular_dependency( repo: TestRepository, tester: CommandTester ) -> None: repo.add_package(get_package("simple-project", "1.1.2")) result = tester.execute("simple-project") assert result == 1 expected = "Cannot add dependency on simple-project to project with the same name." assert expected in tester.io.fetch_error() def test_add_latest_should_strip_out_invalid_pep508_path( tester: CommandTester, repo: TestRepository, mocker: MockerFixture ) -> None: spy = mocker.spy(RequirementsParser, "parse") repo.add_package(get_package("foo", "1.1.1")) repo.add_package(get_package("foo", "1.1.2")) tester.execute("foo@latest") assert tester.status_code == 0 assert "Using version ^1.1.2 for foo" in tester.io.fetch_output() assert spy.call_count == 1 assert spy.call_args_list[0].args[1] == "foo" @pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_latest_should_not_create_duplicate_keys( project_factory: ProjectFactory, repo: TestRepository, command_tester_factory: CommandTesterFactory, project_dependencies: bool, ) -> None: if project_dependencies: pyproject_content = """\ [project] name = "simple-project" version = "1.2.3" dependencies = [ "Foo >= 0.6,<0.7", ] """ else: pyproject_content = """\ [tool.poetry] name = "simple-project" version = "1.2.3" [tool.poetry.dependencies] python = "^3.6" Foo = "^0.6" """ poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) pyproject: dict[str, Any] = poetry.file.read() if project_dependencies: assert "tool" not in pyproject assert pyproject["project"]["dependencies"] == ["Foo >= 0.6,<0.7"] else: assert "project" not in pyproject assert "Foo" in pyproject["tool"]["poetry"]["dependencies"] assert pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^0.6" assert "foo" not in pyproject["tool"]["poetry"]["dependencies"] tester = command_tester_factory("add", poetry=poetry) repo.add_package(get_package("foo", "1.1.2")) tester.execute("foo@latest") updated_pyproject: dict[str, Any] = poetry.file.read() if project_dependencies: assert "tool" not in updated_pyproject assert updated_pyproject["project"]["dependencies"] == ["foo (>=1.1.2,<2.0.0)"] else: assert "project" not in updated_pyproject assert "Foo" in updated_pyproject["tool"]["poetry"]["dependencies"] assert updated_pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^1.1.2" assert "foo" not in updated_pyproject["tool"]["poetry"]["dependencies"] @pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_work_when_adding_existing_package_with_latest_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester, project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() if project_dependencies: pyproject["project"]["dependencies"] = ["foo>1"] else: pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) repo.add_package(get_package("foo", "1.1.2")) tester.execute("foo@latest") expected = """\ Using version ^1.1.2 for foo Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.1.2) Writing lock file """ assert expected in tester.io.fetch_output() pyproject2: dict[str, Any] = app.poetry.file.read() project_content = pyproject2["project"] poetry_content = pyproject2["tool"]["poetry"] if project_dependencies: assert "foo" not in poetry_content["dependencies"] assert project_content["dependencies"] == ["foo (>=1.1.2,<2.0.0)"] else: assert "dependencies" not in project_content assert "foo" in poetry_content["dependencies"] assert poetry_content["dependencies"]["foo"] == "^1.1.2" def test_add_chooses_prerelease_if_only_prereleases_are_available( repo: TestRepository, tester: CommandTester ) -> None: repo.add_package(get_package("foo", "1.2.3b0")) repo.add_package(get_package("foo", "1.2.3b1")) tester.execute("foo") expected = """\ Using version ^1.2.3b1 for foo Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.2.3b1) Writing lock file """ assert expected in tester.io.fetch_output() def test_add_prefers_stable_releases( repo: TestRepository, tester: CommandTester ) -> None: repo.add_package(get_package("foo", "1.2.3")) repo.add_package(get_package("foo", "1.2.4b1")) tester.execute("foo") expected = """\ Using version ^1.2.3 for foo Updating dependencies Resolving dependencies... Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.2.3) Writing lock file """ assert expected in tester.io.fetch_output() def test_add_with_lock(app: PoetryTestApplication, tester: CommandTester) -> None: content_hash = app.poetry.locker._get_content_hash() tester.execute("cachy --lock") expected = """\ Using version ^0.2.0 for cachy Updating dependencies Resolving dependencies... Writing lock file """ assert tester.io.fetch_output() == expected assert content_hash != app.poetry.locker.lock_data["metadata"]["content-hash"] def test_add_keyboard_interrupt_restore_content( poetry_with_up_to_date_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, ) -> None: tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) mocker.patch( "poetry.installation.installer.Installer._execute", side_effect=KeyboardInterrupt(), ) original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() original_lockfile_content = poetry_with_up_to_date_lockfile._locker.lock_data repo.add_package(get_package("docker", "4.3.1")) tester.execute("cachy") assert poetry_with_up_to_date_lockfile.file.read() == original_pyproject_content assert ( poetry_with_up_to_date_lockfile._locker.lock_data == original_lockfile_content ) @pytest.mark.parametrize( "command", [ "cachy --dry-run", "cachy --lock --dry-run", ], ) def test_add_with_dry_run_keep_files_intact( command: str, poetry_with_up_to_date_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() original_lockfile_content = poetry_with_up_to_date_lockfile._locker.lock_data repo.add_package(get_package("docker", "4.3.1")) tester.execute(command) assert poetry_with_up_to_date_lockfile.file.read() == original_pyproject_content assert ( poetry_with_up_to_date_lockfile._locker.lock_data == original_lockfile_content ) def test_add_should_not_change_lock_file_when_dependency_installation_fail( poetry_with_up_to_date_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, ) -> None: tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) repo.add_package(get_package("docker", "4.3.1")) original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() original_lockfile_content = poetry_with_up_to_date_lockfile.locker.lock_data def error(_: Any) -> int: tester.io.write("\n BuildError\n\n") return 1 mocker.patch("poetry.installation.installer.Installer._execute", side_effect=error) tester.execute("cachy") expected = """\ Using version ^0.2.0 for cachy Updating dependencies Resolving dependencies... BuildError """ assert poetry_with_up_to_date_lockfile.file.read() == original_pyproject_content assert poetry_with_up_to_date_lockfile.locker.lock_data == original_lockfile_content assert tester.io.fetch_output() == expected def test_add_with_path_dependency_no_loopiness( poetry_with_path_dependency: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: """https://github.com/python-poetry/poetry/issues/7398""" tester = command_tester_factory("add", poetry=poetry_with_path_dependency) requests_old = get_package("requests", "2.25.1") requests_new = get_package("requests", "2.28.2") repo.add_package(requests_old) repo.add_package(requests_new) with pytest.raises(SolverProblemError): tester.execute("requests") def test_add_extras_are_parsed_and_included( app: PoetryTestApplication, tester: CommandTester, ) -> None: tester.execute('cleo --extras "redis msgpack"') expected = """\ Using version ^0.6.5 for cleo Updating dependencies Resolving dependencies... Package operations: 3 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.6) - Installing redis (3.4.0) - Installing cleo (0.6.5) Writing lock file """ assert tester.io.fetch_output() == expected pyproject: dict[str, Any] = app.poetry.file.read() content = pyproject["tool"]["poetry"] assert "cleo" in content["dependencies"] assert content["dependencies"]["cleo"] == { "version": "^0.6.5", "extras": ["redis", "msgpack"], } @pytest.mark.parametrize( "command", [ "requests --extras security socks", ], ) def test_add_extras_only_accepts_one_package( command: str, tester: CommandTester, repo: TestRepository ) -> None: """ You cannot pass in multiple package values to a single --extras flag.\ e.g. --extras security socks is not allowed. """ repo.add_package(get_package("requests", "2.30.0")) with pytest.raises(ValueError) as e: tester.execute(command) assert ( str(e.value) == "You can only specify one package when using the --extras option" ) @pytest.mark.parametrize("command", ["foo", "foo --lock"]) @pytest.mark.parametrize( ("locked", "expected_docker"), [(True, "4.3.1"), (False, "4.3.2")] ) def test_add_does_not_update_locked_dependencies( repo: TestRepository, poetry_with_up_to_date_lockfile: Poetry, tester: CommandTester, command_tester_factory: CommandTesterFactory, command: str, locked: bool, expected_docker: str, ) -> None: assert isinstance(poetry_with_up_to_date_lockfile.locker, TestLocker) poetry_with_up_to_date_lockfile.locker.locked(locked) tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) docker_locked = get_package("docker", "4.3.1") docker_new = get_package("docker", "4.3.2") docker_dep = get_dependency("docker", ">=4.0.0") foo = get_package("foo", "0.1.0") foo.add_dependency(docker_dep) for package in docker_locked, docker_new, foo: repo.add_package(package) # set correct files to avoid cache refresh if locked: docker_locked.files = ( poetry_with_up_to_date_lockfile.locker.locked_repository() .package("docker", Version.parse("4.3.1")) .files ) tester.execute(command) lock_data = poetry_with_up_to_date_lockfile.locker.lock_data docker_locked_after_command = next( p for p in lock_data["package"] if p["name"] == "docker" ) assert docker_locked_after_command["version"] == expected_docker def test_add_creates_dependencies_array_if_necessary( project_factory: ProjectFactory, repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: pyproject_content = """\ [project] name = "simple-project" version = "1.2.3" [project.optional-dependencies] test = ["foo"] """ poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) repo.add_package(get_package("foo", "2.0")) repo.add_package(get_package("bar", "2.0")) tester = command_tester_factory("add", poetry=poetry) tester.execute("bar>=1.0") updated_pyproject: dict[str, Any] = poetry.file.read() assert updated_pyproject["project"]["dependencies"] == ["bar (>=1.0)"] @pytest.mark.parametrize("has_poetry_section", [True, False]) def test_add_does_not_add_poetry_dependencies_if_not_necessary( project_factory: ProjectFactory, repo: TestRepository, command_tester_factory: CommandTesterFactory, has_poetry_section: bool, ) -> None: pyproject_content = """\ [project] name = "simple-project" version = "1.2.3" dependencies = [ "foo >= 1.0", ] """ if has_poetry_section: pyproject_content += """\ [tool.poetry] packages = [ { include = "simple" } ] """ poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) pyproject: dict[str, Any] = poetry.file.read() if has_poetry_section: assert "dependencies" not in pyproject["tool"]["poetry"] else: assert "tool" not in pyproject repo.add_package(get_package("foo", "2.0")) repo.add_package(get_package("bar", "2.0")) tester = command_tester_factory("add", poetry=poetry) tester.execute("bar>=1.0 --platform linux") updated_pyproject: dict[str, Any] = poetry.file.read() if has_poetry_section: assert "dependencies" not in pyproject["tool"]["poetry"] else: assert "tool" not in pyproject assert updated_pyproject["project"]["dependencies"] == [ "foo >= 1.0", 'bar (>=1.0) ; sys_platform == "linux"', ] @pytest.mark.parametrize("has_poetry_section", [True, False]) def test_add_poetry_dependencies_if_necessary( project_factory: ProjectFactory, repo: TestRepository, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, has_poetry_section: bool, ) -> None: pyproject_content = """\ [project] name = "simple-project" version = "1.2.3" dependencies = [ "foo >= 1.0", ] """ if has_poetry_section: pyproject_content += """\ [tool.poetry] packages = [ { include = "simple" } ] """ poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) pyproject: dict[str, Any] = poetry.file.read() if has_poetry_section: assert "dependencies" not in pyproject["tool"]["poetry"] else: assert "tool" not in pyproject repo.add_package(get_package("foo", "2.0")) other_repo = LegacyRepository(name="my-index", url="https://my-index.fake") poetry.pool.add_repository(other_repo) package = Package( "bar", "2.0", source_type="legacy", source_url=other_repo.url, source_reference=other_repo.name, ) mocker.patch.object(other_repo, "package", return_value=package) mocker.patch.object(other_repo, "_find_packages", wraps=lambda _, name: [package]) repo.add_package(package) tester = command_tester_factory("add", poetry=poetry) tester.execute("bar>=1.0 --platform linux --allow-prereleases --source my-index") updated_pyproject: dict[str, Any] = poetry.file.read() if has_poetry_section: assert "dependencies" not in pyproject["tool"]["poetry"] else: assert "tool" not in pyproject assert updated_pyproject["project"]["dependencies"] == [ "foo >= 1.0", 'bar (>=1.0) ; sys_platform == "linux"', ] assert updated_pyproject["tool"]["poetry"]["dependencies"] == { "bar": { "platform": "linux", "source": "my-index", "allow-prereleases": True, } } ================================================ FILE: tests/console/commands/test_build.py ================================================ from __future__ import annotations import shutil import tarfile from typing import TYPE_CHECKING import pytest from cleo.io.null_io import NullIO from cleo.testers.application_tester import ApplicationTester from poetry.console.application import Application from poetry.console.commands.build import BuildCommand from poetry.console.commands.build import BuildHandler from poetry.console.commands.build import BuildOptions from poetry.factory import Factory from poetry.utils.helpers import remove_directory from tests.helpers import with_working_directory if TYPE_CHECKING: from pathlib import Path from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.poetry import Poetry from poetry.utils.env import VirtualEnv from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter @pytest.fixture def tmp_project_path(tmp_path: Path) -> Path: return tmp_path / "project" @pytest.fixture def tmp_poetry(tmp_project_path: Path, fixture_dir: FixtureDirGetter) -> Poetry: # copy project so that we start with a clean directory shutil.copytree(fixture_dir("simple_project"), tmp_project_path) poetry = Factory().create_poetry(tmp_project_path) return poetry @pytest.fixture def tmp_tester( tmp_poetry: Poetry, command_tester_factory: CommandTesterFactory ) -> CommandTester: return command_tester_factory("build", tmp_poetry) def get_package_glob(poetry: Poetry, local_version: str | None = None) -> str: version = poetry.package.version if local_version: version = version.replace(local=local_version) return f"{poetry.package.name.replace('-', '_')}-{version}*" def test_build_format_is_not_valid(tmp_tester: CommandTester) -> None: with pytest.raises(ValueError, match=r"Invalid format.*"): tmp_tester.execute("--format not_valid") @pytest.mark.parametrize("format", ["sdist", "wheel", "all"]) def test_build_creates_packages_in_dist_directory_if_no_output_is_specified( tmp_tester: CommandTester, tmp_project_path: Path, tmp_poetry: Poetry, format: str ) -> None: shutil.rmtree(tmp_project_path / "dist") tmp_tester.execute(f"--format {format}") build_artifacts = tuple( (tmp_project_path / "dist").glob(get_package_glob(tmp_poetry)) ) assert len(build_artifacts) > 0 assert all(archive.exists() for archive in build_artifacts) def test_build_with_local_version_label( tmp_tester: CommandTester, tmp_project_path: Path, tmp_poetry: Poetry ) -> None: shutil.rmtree(tmp_project_path / "dist") local_version_label = "local-version" tmp_tester.execute(f"--local-version {local_version_label}") build_artifacts = tuple( (tmp_project_path / "dist").glob( get_package_glob(tmp_poetry, local_version=local_version_label) ) ) assert len(build_artifacts) > 0 assert all(archive.exists() for archive in build_artifacts) @pytest.mark.parametrize("clean", [True, False]) def test_build_with_clean( tmp_tester: CommandTester, tmp_project_path: Path, tmp_poetry: Poetry, clean: bool ) -> None: dist_dir = tmp_project_path.joinpath("dist") dist_dir.joinpath("hello").touch(exist_ok=True) tmp_tester.execute("--clean" if clean else "") build_artifacts = tuple(dist_dir.glob("*")) assert len(build_artifacts) == 2 if clean else 3 assert all(archive.exists() for archive in build_artifacts) def test_build_with_clean_non_existing_output( tmp_tester: CommandTester, tmp_project_path: Path, tmp_poetry: Poetry ) -> None: dist_dir = tmp_project_path.joinpath("dist") remove_directory(dist_dir, force=True) assert not dist_dir.exists() tmp_tester.execute("--clean") build_artifacts = tuple(dist_dir.glob("*")) assert len(build_artifacts) == 2 assert all(archive.exists() for archive in build_artifacts) def test_build_not_possible_in_non_package_mode( fixture_dir: FixtureDirGetter, command_tester_factory: CommandTesterFactory, ) -> None: source_dir = fixture_dir("non_package_mode") poetry = Factory().create_poetry(source_dir) tester = command_tester_factory("build", poetry) assert tester.execute() == 1 assert ( tester.io.fetch_error() == "Building a package is not possible in non-package mode.\n" ) def test_build_with_multiple_readme_files( fixture_dir: FixtureDirGetter, tmp_path: Path, tmp_venv: VirtualEnv, command_tester_factory: CommandTesterFactory, ) -> None: source_dir = fixture_dir("with_multiple_readme_files") target_dir = tmp_path / "project" shutil.copytree(str(source_dir), str(target_dir)) poetry = Factory().create_poetry(target_dir) tester = command_tester_factory("build", poetry, environment=tmp_venv) tester.execute() build_dir = target_dir / "dist" assert build_dir.exists() sdist_file = build_dir / "my_package-0.1.tar.gz" assert sdist_file.exists() assert sdist_file.stat().st_size > 0 (wheel_file,) = build_dir.glob("my_package-0.1-*.whl") assert wheel_file.exists() assert wheel_file.stat().st_size > 0 with tarfile.open(sdist_file) as tf: sdist_content = tf.getnames() assert "my_package-0.1/README-1.rst" in sdist_content assert "my_package-0.1/README-2.rst" in sdist_content @pytest.mark.parametrize( "output_dir", [None, "dist", "test/dir", "../dist", "absolute"] ) def test_build_output_option( tmp_tester: CommandTester, tmp_project_path: Path, tmp_poetry: Poetry, output_dir: str, ) -> None: shutil.rmtree(tmp_project_path / "dist") if output_dir is None: tmp_tester.execute() build_dir = tmp_project_path / "dist" elif output_dir == "absolute": tmp_tester.execute(f"--output {tmp_project_path / 'tmp/dist'}") build_dir = tmp_project_path / "tmp/dist" else: tmp_tester.execute(f"--output {output_dir}") build_dir = tmp_project_path / output_dir build_artifacts = tuple(build_dir.glob(get_package_glob(tmp_poetry))) assert len(build_artifacts) > 0 assert all(archive.exists() for archive in build_artifacts) def test_build_relative_directory_src_layout( tmp_path: Path, fixture_dir: FixtureDirGetter ) -> None: tmp_project_path = tmp_path / "project" with with_working_directory(fixture_dir("simple_project"), tmp_project_path): shutil.rmtree(tmp_project_path / "dist") (tmp_project_path / "src").mkdir() (tmp_project_path / "simple_project").rename( tmp_project_path / "src" / "simple_project" ) # We have to use ApplicationTester because CommandTester # initializes Poetry before passing the directory. app = Application() tester = ApplicationTester(app) tester.execute("build --project .") build_dir = tmp_project_path / "dist" assert len(list(build_dir.iterdir())) == 2 def test_build_options_validate_formats() -> None: with pytest.raises(ValueError, match="Invalid format: UNKNOWN"): _ = BuildOptions(clean=True, formats=["sdist", "UNKNOWN"], output="dist") # type: ignore[list-item] def test_prepare_config_settings() -> None: config_settings = BuildCommand._prepare_config_settings( local_version="42", config_settings=["setting_1=value_1", "setting_2=value_2"], io=NullIO(), ) assert config_settings == { "local-version": "42", "setting_1": "value_1", "setting_2": "value_2", } def test_prepare_config_settings_raise_on_invalid_setting() -> None: with pytest.raises(ValueError, match="Invalid config setting format: value_2"): _ = BuildCommand._prepare_config_settings( local_version="42", config_settings=["setting_1=value_1", "value_2"], io=NullIO(), ) @pytest.mark.parametrize( ["fmt", "expected_formats"], [ ("all", ["sdist", "wheel"]), (None, ["sdist", "wheel"]), ("sdist", ["sdist"]), ("wheel", ["wheel"]), ], ) def test_prepare_formats(fmt: str | None, expected_formats: list[str]) -> None: formats = BuildCommand._prepare_formats(fmt) assert formats == expected_formats @pytest.mark.parametrize( ["project", "isolated_build"], [ ("core_in_range", False), ("core_not_in_range", True), ("has_build_script", True), ("multiple_build_deps", True), ("no_core", True), ("core_from_git", True), ("no_build_system", False), ("no_build_backend", False), ], ) def test_requires_isolated_build( project: str, isolated_build: bool, fixture_dir: FixtureDirGetter, mocker: MockerFixture, ) -> None: poetry = Factory().create_poetry(fixture_dir(f"build_systems/{project}")) handler = BuildHandler(poetry=poetry, env=mocker.Mock(), io=NullIO()) assert handler._requires_isolated_build() is isolated_build def test_build_handler_build_isolated( fixture_dir: FixtureDirGetter, mocker: MockerFixture ) -> None: from build import ProjectBuilder poetry = Factory().create_poetry(fixture_dir("build_systems/has_build_script")) mock_builder = mocker.MagicMock(spec=ProjectBuilder) mock_isolated_builder = mocker.patch( "poetry.console.commands.build.isolated_builder" ) mock_isolated_builder.return_value.__enter__.return_value = mock_builder handler = BuildHandler(poetry=poetry, env=mocker.Mock(), io=NullIO()) handler.build(BuildOptions(clean=True, formats=["wheel"], output="dist")) assert mock_builder.build.call_count == 1 ================================================ FILE: tests/console/commands/test_check.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.factory import Factory from poetry.packages import Locker from poetry.toml import TOMLFile if TYPE_CHECKING: from collections.abc import Iterator from pathlib import Path from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.poetry import Poetry from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import SetProjectContext @pytest.fixture def poetry_simple_project(set_project_context: SetProjectContext) -> Iterator[Poetry]: with set_project_context("simple_project", in_place=False) as cwd: yield Factory().create_poetry(cwd) @pytest.fixture def poetry_with_outdated_lockfile( set_project_context: SetProjectContext, ) -> Iterator[Poetry]: with set_project_context("outdated_lock", in_place=False) as cwd: yield Factory().create_poetry(cwd) @pytest.fixture def poetry_with_up_to_date_lockfile( set_project_context: SetProjectContext, ) -> Iterator[Poetry]: with set_project_context("up_to_date_lock", in_place=False) as cwd: yield Factory().create_poetry(cwd) @pytest.fixture def poetry_with_pypi_reference( set_project_context: SetProjectContext, ) -> Iterator[Poetry]: with set_project_context("pypi_reference", in_place=False) as cwd: yield Factory().create_poetry(cwd) @pytest.fixture def poetry_with_invalid_pyproject( set_project_context: SetProjectContext, ) -> Iterator[Poetry]: with set_project_context("invalid_pyproject", in_place=False) as cwd: yield Factory().create_poetry(cwd) @pytest.fixture() def tester( command_tester_factory: CommandTesterFactory, poetry_simple_project: Poetry ) -> CommandTester: return command_tester_factory("check", poetry=poetry_simple_project) def test_check_valid(tester: CommandTester) -> None: tester.execute() expected = """\ All set! """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize( ["args", "expected_status"], [ ([], 0), (["--strict"], 1), ], ) def test_check_valid_legacy( args: list[str], expected_status: int, mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter, ) -> None: mocker.patch( "poetry.poetry.Poetry.file", return_value=TOMLFile(fixture_dir("simple_project_legacy") / "pyproject.toml"), new_callable=mocker.PropertyMock, ) tester.execute(" ".join(args)) expected = ( "Warning: [tool.poetry.name] is deprecated. Use [project.name] instead.\n" "Warning: [tool.poetry.version] is set but 'version' is not in " "[project.dynamic]. If it is static use [project.version]. If it is dynamic, " "add 'version' to [project.dynamic].\n" "If you want to set the version dynamically via `poetry build " "--local-version` or you are using a plugin, which sets the version " "dynamically, you should define the version in [tool.poetry] and add " "'version' to [project.dynamic].\n" "Warning: [tool.poetry.description] is deprecated. Use [project.description] " "instead.\n" "Warning: [tool.poetry.readme] is set but 'readme' is not in " "[project.dynamic]. If it is static use [project.readme]. If it is dynamic, " "add 'readme' to [project.dynamic].\n" "If you want to define multiple readmes, you should define them in " "[tool.poetry] and add 'readme' to [project.dynamic].\n" "Warning: [tool.poetry.license] is deprecated. Use [project.license] instead.\n" "Warning: [tool.poetry.authors] is deprecated. Use [project.authors] instead.\n" "Warning: [tool.poetry.keywords] is deprecated. Use [project.keywords] " "instead.\n" "Warning: [tool.poetry.classifiers] is set but 'classifiers' is not in " "[project.dynamic]. If it is static use [project.classifiers]. If it is " "dynamic, add 'classifiers' to [project.dynamic].\n" "ATTENTION: Per default Poetry determines classifiers for supported Python " "versions and license automatically. If you define classifiers in [project], " "you disable the automatic enrichment. In other words, you have to define all " "classifiers manually. If you want to use Poetry's automatic enrichment of " "classifiers, you should define them in [tool.poetry] and add 'classifiers' " "to [project.dynamic].\n" "Warning: [tool.poetry.homepage] is deprecated. Use [project.urls] instead.\n" "Warning: [tool.poetry.repository] is deprecated. Use [project.urls] instead.\n" "Warning: [tool.poetry.documentation] is deprecated. Use [project.urls] " "instead.\n" "Warning: Defining console scripts in [tool.poetry.scripts] is deprecated. " "Use [project.scripts] instead. ([tool.poetry.scripts] should only be used " "for scripts of type 'file').\n" ) assert tester.io.fetch_error() == expected assert tester.status_code == expected_status def test_check_invalid_dep_name_same_as_project_name( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter ) -> None: mocker.patch( "poetry.poetry.Poetry.file", return_value=TOMLFile( fixture_dir("invalid_pyproject_dep_name") / "pyproject.toml" ), new_callable=mocker.PropertyMock, ) tester.execute("") expected = """\ Error: Project name (invalid) is same as one of its dependencies """ assert tester.io.fetch_error() == expected def test_check_invalid( tester: CommandTester, fixture_dir: FixtureDirGetter, command_tester_factory: CommandTesterFactory, poetry_with_invalid_pyproject: Poetry, ) -> None: tester = command_tester_factory("check", poetry=poetry_with_invalid_pyproject) tester.execute("--lock") expected = """\ Error: Unrecognized classifiers: ['Intended Audience :: Clowns']. Error: Declared README file does not exist: never/exists.md Error: Invalid source "not-exists" referenced in dependencies. Error: Invalid source "not-exists2" referenced in dependencies. Error: poetry.lock was not found. Warning: [project.license] is not a valid SPDX expression.\ This is deprecated and will raise an error in the future. Warning: A wildcard Python dependency is ambiguous.\ Consider specifying a more explicit one. Warning: The "pendulum" dependency specifies the "allows-prereleases" property,\ which is deprecated. Use "allow-prereleases" instead. Warning: Deprecated classifier 'Natural Language :: Ukranian'.\ Must be replaced by ['Natural Language :: Ukrainian']. Warning: Deprecated classifier\ 'Topic :: Communications :: Chat :: AOL Instant Messenger'.\ Must be removed. """ assert tester.io.fetch_error() == expected def test_check_private( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter ) -> None: mocker.patch( "poetry.poetry.Poetry.file", return_value=TOMLFile(fixture_dir("private_pyproject") / "pyproject.toml"), new_callable=mocker.PropertyMock, ) tester.execute() expected = """\ All set! """ assert tester.io.fetch_output() == expected def test_check_non_package_mode( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter ) -> None: mocker.patch( "poetry.poetry.Poetry.file", return_value=TOMLFile(fixture_dir("non_package_mode") / "pyproject.toml"), new_callable=mocker.PropertyMock, ) tester.execute() expected = """\ All set! """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize( ("options", "expected", "expected_status"), [ ("", "All set!\n", 0), ("--lock", "Error: poetry.lock was not found.\n", 1), ], ) def test_check_lock_missing( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter, options: str, expected: str, expected_status: int, ) -> None: mocker.patch( "poetry.poetry.Poetry.file", return_value=TOMLFile(fixture_dir("private_pyproject") / "pyproject.toml"), new_callable=mocker.PropertyMock, ) status_code = tester.execute(options) assert status_code == expected_status if status_code == 0: assert tester.io.fetch_output() == expected else: assert tester.io.fetch_error() == expected @pytest.mark.parametrize("options", ["", "--lock"]) def test_check_lock_outdated( command_tester_factory: CommandTesterFactory, poetry_with_outdated_lockfile: Poetry, options: str, ) -> None: locker = Locker( lock=poetry_with_outdated_lockfile.pyproject.file.path.parent / "poetry.lock", pyproject_data=poetry_with_outdated_lockfile.locker._pyproject_data, ) poetry_with_outdated_lockfile.set_locker(locker) tester = command_tester_factory("check", poetry=poetry_with_outdated_lockfile) status_code = tester.execute(options) expected = ( "Error: pyproject.toml changed significantly since poetry.lock was last generated. " "Run `poetry lock` to fix the lock file.\n" ) assert tester.io.fetch_error() == expected # exit with an error assert status_code == 1 @pytest.mark.parametrize("options", ["", "--lock"]) def test_check_lock_up_to_date( command_tester_factory: CommandTesterFactory, poetry_with_up_to_date_lockfile: Poetry, options: str, ) -> None: locker = Locker( lock=poetry_with_up_to_date_lockfile.pyproject.file.path.parent / "poetry.lock", pyproject_data=poetry_with_up_to_date_lockfile.locker._pyproject_data, ) poetry_with_up_to_date_lockfile.set_locker(locker) tester = command_tester_factory("check", poetry=poetry_with_up_to_date_lockfile) status_code = tester.execute(options) expected = "All set!\n" assert tester.io.fetch_output() == expected # exit with an error assert status_code == 0 def test_check_does_not_error_on_pypi_reference( command_tester_factory: CommandTesterFactory, poetry_with_pypi_reference: Poetry, ) -> None: tester = command_tester_factory("check", poetry=poetry_with_pypi_reference) status_code = tester.execute("") assert tester.io.fetch_output() == "All set!\n" assert status_code == 0 @pytest.fixture(params=["project_str", "project_dict", "poetry_str", "poetry_array"]) def pyproject_with_readme_file(tmp_path: Path, request: pytest.FixtureRequest) -> Path: pyproject_content = """\ [project] name = "test" version = "1.0.0" """ if request.param == "project_str": pyproject_content += 'readme = "README.md"\n' elif request.param == "project_dict": pyproject_content += ( 'readme = { file = "README.md", content-type = "text/markdown" }\n' ) elif request.param == "poetry_str": pyproject_content += """ [tool.poetry] readme = "README.md" """ elif request.param == "poetry_array": pyproject_content += """ [tool.poetry] readme = ["README.md"] """ else: raise ValueError(f"Unknown readme type: {request.param}") pyproject_path = tmp_path / "pyproject.toml" pyproject_path.write_text(pyproject_content, encoding="utf-8") return pyproject_path @pytest.mark.parametrize("readme_exists", [True, False]) def test_check_readme_file_exists( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter, tmp_path: Path, pyproject_with_readme_file: Path, readme_exists: bool, ) -> None: if readme_exists: readme_path = tmp_path / "README.md" readme_path.write_text("README", encoding="utf-8") mocker.patch( "poetry.poetry.Poetry.file", return_value=TOMLFile(pyproject_with_readme_file), new_callable=mocker.PropertyMock, ) result = tester.execute() if readme_exists: assert result == 0 assert "Declared README file does not exist" not in tester.io.fetch_error() else: assert result == 1 assert ( "Declared README file does not exist: README.md" in tester.io.fetch_error() ) @pytest.fixture(params=["project_str", "project_dict", "poetry_str", "poetry_array"]) def pyproject_with_empty_readme_file( tmp_path: Path, request: pytest.FixtureRequest ) -> Path: pyproject_content = """\ [project] name = "test" version = "1.0.0" """ if request.param == "project_str": pyproject_content += 'readme = ""\n' elif request.param == "project_dict": pyproject_content += 'readme = { file = "", content-type = "text/markdown" }\n' elif request.param == "poetry_str": pyproject_content += """ [tool.poetry] readme = "" """ elif request.param == "poetry_array": pyproject_content += """ [tool.poetry] readme = [""] """ else: raise ValueError(f"Unknown readme type: {request.param}") pyproject_path = tmp_path / "pyproject.toml" pyproject_path.write_text(pyproject_content, encoding="utf-8") return pyproject_path def test_check_project_readme_as_text( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pyproject_content = """[project] name = "test" version = "1.0.0" readme = { content-type = "text/markdown", text = "README" } """ pyproject_path = tmp_path / "pyproject.toml" pyproject_path.write_text(pyproject_content, encoding="utf-8") mocker.patch( "poetry.poetry.Poetry.file", return_value=TOMLFile(pyproject_path), new_callable=mocker.PropertyMock, ) result = tester.execute() assert result == 0 assert "Declared README file does not exist" not in tester.io.fetch_error() def test_check_poetry_readme_multiple( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pyproject_content = """[project] name = "test" version = "1.0.0" dynamic = ["readme"] [tool.poetry] readme = ["README1.md", "README2.md", "README3.md", "README4.md"] """ pyproject_path = tmp_path / "pyproject.toml" pyproject_path.write_text(pyproject_content, encoding="utf-8") (tmp_path / "README2.md").write_text("README 2", encoding="utf-8") (tmp_path / "README3.md").write_text("README 3", encoding="utf-8") mocker.patch( "poetry.poetry.Poetry.file", return_value=TOMLFile(pyproject_path), new_callable=mocker.PropertyMock, ) result = tester.execute() assert result == 1 assert tester.io.fetch_error() == ( "Error: Declared README file does not exist: README1.md\n" "Error: Declared README file does not exist: README4.md\n" ) ================================================ FILE: tests/console/commands/test_config.py ================================================ from __future__ import annotations import json import os import textwrap from pathlib import Path from typing import TYPE_CHECKING import pytest from deepdiff.diff import DeepDiff from poetry.core.pyproject.exceptions import PyProjectError from poetry.config.config_source import ConfigSource from poetry.config.config_source import PropertyNotFoundError from poetry.console.commands.config import ConfigCommand from poetry.console.commands.install import InstallCommand from poetry.factory import Factory from poetry.repositories.legacy_repository import LegacyRepository from tests.conftest import Config from tests.helpers import flatten_dict if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.config.dict_config_source import DictConfigSource from poetry.poetry import Poetry from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("config") def test_config_command_in_sync_with_config_class() -> None: assert set(ConfigCommand().unique_config_values) == set( flatten_dict(Config.default_config) ) def test_show_config_with_local_config_file_empty( tester: CommandTester, mocker: MockerFixture ) -> None: mocker.patch( "poetry.factory.Factory.create_poetry", side_effect=PyProjectError("[tool.poetry] section not found"), ) tester.execute() assert tester.io.fetch_output() == "" def test_list_displays_default_value_if_not_set( tester: CommandTester, config_cache_dir: Path, config_data_dir: Path ) -> None: tester.execute("--list") cache_dir = json.dumps(str(config_cache_dir)) data_dir = json.dumps(str(config_data_dir)) venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs")) expected = f"""cache-dir = {cache_dir} data-dir = {data_dir} installer.max-workers = null installer.no-binary = null installer.only-binary = null installer.parallel = true installer.re-resolve = false keyring.enabled = true python.installation-dir = {json.dumps(str(Path("{data-dir}/python")))} # {config_data_dir / "python"} requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / "virtualenvs"} virtualenvs.prompt = "{{project_name}}-py{{python_version}}" virtualenvs.use-poetry-python = false """ assert tester.io.fetch_output() == expected def test_list_displays_set_get_setting( tester: CommandTester, config: Config, config_cache_dir: Path, config_data_dir: Path ) -> None: tester.execute("virtualenvs.create false") tester.execute("--list") cache_dir = json.dumps(str(config_cache_dir)) data_dir = json.dumps(str(config_data_dir)) venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs")) expected = f"""cache-dir = {cache_dir} data-dir = {data_dir} installer.max-workers = null installer.no-binary = null installer.only-binary = null installer.parallel = true installer.re-resolve = false keyring.enabled = true python.installation-dir = {json.dumps(str(Path("{data-dir}/python")))} # {config_data_dir / "python"} requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false virtualenvs.create = false virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / "virtualenvs"} virtualenvs.prompt = "{{project_name}}-py{{python_version}}" virtualenvs.use-poetry-python = false """ assert config.set_config_source.call_count == 0 # type: ignore[attr-defined] assert tester.io.fetch_output() == expected def test_cannot_set_with_multiple_values(tester: CommandTester) -> None: with pytest.raises(RuntimeError) as e: tester.execute("virtualenvs.create false true") assert str(e.value) == "You can only pass one value." def test_cannot_set_invalid_value(tester: CommandTester) -> None: with pytest.raises(RuntimeError) as e: tester.execute("virtualenvs.create foo") assert str(e.value) == '"foo" is an invalid value for virtualenvs.create' def test_cannot_unset_with_value(tester: CommandTester) -> None: with pytest.raises(RuntimeError) as e: tester.execute("virtualenvs.create false --unset") assert str(e.value) == "You can not combine a setting value with --unset" def test_unset_setting( tester: CommandTester, config: Config, config_cache_dir: Path, config_data_dir: Path ) -> None: tester.execute("virtualenvs.path /some/path") tester.execute("virtualenvs.path --unset") tester.execute("--list") cache_dir = json.dumps(str(config_cache_dir)) data_dir = json.dumps(str(config_data_dir)) venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs")) expected = f"""cache-dir = {cache_dir} data-dir = {data_dir} installer.max-workers = null installer.no-binary = null installer.only-binary = null installer.parallel = true installer.re-resolve = false keyring.enabled = true python.installation-dir = {json.dumps(str(Path("{data-dir}/python")))} # {config_data_dir / "python"} requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / "virtualenvs"} virtualenvs.prompt = "{{project_name}}-py{{python_version}}" virtualenvs.use-poetry-python = false """ assert config.set_config_source.call_count == 0 # type: ignore[attr-defined] assert tester.io.fetch_output() == expected def test_unset_repo_setting( tester: CommandTester, config: Config, config_cache_dir: Path, config_data_dir: Path ) -> None: tester.execute("repositories.foo.url https://bar.com/simple/") tester.execute("repositories.foo.url --unset ") tester.execute("--list") cache_dir = json.dumps(str(config_cache_dir)) data_dir = json.dumps(str(config_data_dir)) venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs")) expected = f"""cache-dir = {cache_dir} data-dir = {data_dir} installer.max-workers = null installer.no-binary = null installer.only-binary = null installer.parallel = true installer.re-resolve = false keyring.enabled = true python.installation-dir = {json.dumps(str(Path("{data-dir}/python")))} # {config_data_dir / "python"} requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / "virtualenvs"} virtualenvs.prompt = "{{project_name}}-py{{python_version}}" virtualenvs.use-poetry-python = false """ assert config.set_config_source.call_count == 0 # type: ignore[attr-defined] assert tester.io.fetch_output() == expected def test_unset_value_not_exists(tester: CommandTester) -> None: with pytest.raises(ValueError) as e: tester.execute("foobar --unset") assert str(e.value) == "Setting foobar does not exist" @pytest.mark.parametrize( ("value", "expected"), [ ("virtualenvs.create", "true\n"), ("repositories.foo.url", "{'url': 'https://bar.com/simple/'}\n"), ], ) def test_display_single_setting( tester: CommandTester, value: str, expected: str | bool ) -> None: tester.execute("repositories.foo.url https://bar.com/simple/") tester.execute(value) assert tester.io.fetch_output() == expected def test_display_single_local_setting( command_tester_factory: CommandTesterFactory, fixture_dir: FixtureDirGetter ) -> None: tester = command_tester_factory( "config", poetry=Factory().create_poetry(fixture_dir("with_local_config")) ) tester.execute("virtualenvs.create") expected = """false """ assert tester.io.fetch_output() == expected def test_display_empty_repositories_setting( command_tester_factory: CommandTesterFactory, fixture_dir: FixtureDirGetter ) -> None: tester = command_tester_factory( "config", poetry=Factory().create_poetry(fixture_dir("with_local_config")), ) tester.execute("repositories") expected = """{} """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize( ("setting", "expected"), [ ("repositories", "You cannot remove the [repositories] section"), ("repositories.test", "There is no test repository defined"), ], ) def test_unset_nonempty_repositories_section( tester: CommandTester, setting: str, expected: str ) -> None: tester.execute("repositories.foo.url https://bar.com/simple/") with pytest.raises(ValueError) as e: tester.execute(f"{setting} --unset") assert str(e.value) == expected def test_set_malformed_repositories_setting( tester: CommandTester, ) -> None: with pytest.raises(ValueError) as e: tester.execute("repositories.foo bar baz") assert ( str(e.value) == "You must pass the url. Example: poetry config repositories.foo" " https://bar.com" ) @pytest.mark.parametrize( ("setting", "expected"), [ ("repositories.foo", "There is no foo repository defined"), ("foo", "There is no foo setting."), ], ) def test_display_undefined_setting( tester: CommandTester, setting: str, expected: str ) -> None: with pytest.raises(ValueError) as e: tester.execute(setting) assert str(e.value) == expected def test_list_displays_set_get_local_setting( tester: CommandTester, config: Config, config_cache_dir: Path, config_data_dir: Path, ) -> None: tester.execute("virtualenvs.create false --local") tester.execute("--list") cache_dir = json.dumps(str(config_cache_dir)) data_dir = json.dumps(str(config_data_dir)) venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs")) expected = f"""cache-dir = {cache_dir} data-dir = {data_dir} installer.max-workers = null installer.no-binary = null installer.only-binary = null installer.parallel = true installer.re-resolve = false keyring.enabled = true python.installation-dir = {json.dumps(str(Path("{data-dir}/python")))} # {config_data_dir / "python"} requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false virtualenvs.create = false virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / "virtualenvs"} virtualenvs.prompt = "{{project_name}}-py{{python_version}}" virtualenvs.use-poetry-python = false """ assert config.set_config_source.call_count == 1 # type: ignore[attr-defined] assert tester.io.fetch_output() == expected def test_list_must_not_display_sources_from_pyproject_toml( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, command_tester_factory: CommandTesterFactory, config_cache_dir: Path, config_data_dir: Path, ) -> None: source = fixture_dir("with_primary_source_implicit") pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") poetry = project_factory("foo", pyproject_content=pyproject_content) tester = command_tester_factory("config", poetry=poetry) tester.execute("--list") cache_dir = json.dumps(str(config_cache_dir)) data_dir = json.dumps(str(config_data_dir)) venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs")) expected = f"""cache-dir = {cache_dir} data-dir = {data_dir} installer.max-workers = null installer.no-binary = null installer.only-binary = null installer.parallel = true installer.re-resolve = false keyring.enabled = true python.installation-dir = {json.dumps(str(Path("{data-dir}/python")))} # {config_data_dir / "python"} repositories.foo.url = "https://foo.bar/simple/" requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / "virtualenvs"} virtualenvs.prompt = "{{project_name}}-py{{python_version}}" virtualenvs.use-poetry-python = false """ assert tester.io.fetch_output() == expected def test_set_http_basic( tester: CommandTester, auth_config_source: DictConfigSource ) -> None: tester.execute("http-basic.foo username password") tester.execute("--list") assert auth_config_source.config["http-basic"]["foo"] == { "username": "username", "password": "password", } def test_unset_http_basic( tester: CommandTester, auth_config_source: DictConfigSource ) -> None: tester.execute("http-basic.foo username password") tester.execute("http-basic.foo --unset") tester.execute("--list") assert "foo" not in auth_config_source.config["http-basic"] def test_set_http_basic_unsuccessful_multiple_values( tester: CommandTester, ) -> None: with pytest.raises(ValueError) as e: tester.execute("http-basic.foo username password password") assert str(e.value) == "Expected one or two arguments (username, password), got 3" def test_set_pypi_token( tester: CommandTester, auth_config_source: DictConfigSource ) -> None: tester.execute("pypi-token.pypi mytoken") tester.execute("--list") assert auth_config_source.config["pypi-token"]["pypi"] == "mytoken" def test_unset_pypi_token( tester: CommandTester, auth_config_source: DictConfigSource ) -> None: tester.execute("pypi-token.pypi mytoken") tester.execute("pypi-token.pypi --unset") tester.execute("--list") assert "pypi" not in auth_config_source.config["pypi-token"] def test_set_pypi_token_unsuccessful_multiple_values( tester: CommandTester, ) -> None: with pytest.raises(ValueError) as e: tester.execute("pypi-token.pypi mytoken mytoken") assert str(e.value) == "Expected only one argument (token), got 2" def test_set_pypi_token_no_values( tester: CommandTester, ) -> None: with pytest.raises(ValueError) as e: tester.execute("pypi-token.pypi") assert str(e.value) == "Expected a value for pypi-token.pypi setting." def test_set_client_cert( tester: CommandTester, auth_config_source: DictConfigSource, mocker: MockerFixture, ) -> None: mocker.spy(ConfigSource, "__init__") tester.execute("certificates.foo.client-cert path/to/cert.pem") assert ( auth_config_source.config["certificates"]["foo"]["client-cert"] == "path/to/cert.pem" ) def test_set_client_cert_unsuccessful_multiple_values( tester: CommandTester, mocker: MockerFixture, ) -> None: mocker.spy(ConfigSource, "__init__") with pytest.raises(ValueError) as e: tester.execute("certificates.foo.client-cert path/to/cert.pem path/to/cert.pem") assert str(e.value) == "You must pass exactly 1 value" @pytest.mark.parametrize( ("value", "result"), [ ("path/to/ca.pem", "path/to/ca.pem"), ("true", True), ("false", False), ], ) def test_set_cert( tester: CommandTester, auth_config_source: DictConfigSource, mocker: MockerFixture, value: str, result: str | bool, ) -> None: mocker.spy(ConfigSource, "__init__") tester.execute(f"certificates.foo.cert {value}") assert auth_config_source.config["certificates"]["foo"]["cert"] == result def test_unset_cert( tester: CommandTester, auth_config_source: DictConfigSource, mocker: MockerFixture, ) -> None: mocker.spy(ConfigSource, "__init__") tester.execute("certificates.foo.cert path/to/ca.pem") assert "cert" in auth_config_source.config["certificates"]["foo"] tester.execute("certificates.foo.cert --unset") assert "cert" not in auth_config_source.config["certificates"]["foo"] def test_config_installer_parallel( tester: CommandTester, command_tester_factory: CommandTesterFactory ) -> None: tester.execute("--local installer.parallel") assert tester.io.fetch_output().strip() == "true" command = command_tester_factory("install")._command assert isinstance(command, InstallCommand) workers = command.installer._executor._max_workers assert workers > 1 tester.io.clear_output() tester.execute("--local installer.parallel false") tester.execute("--local installer.parallel") assert tester.io.fetch_output().strip() == "false" command = command_tester_factory("install")._command assert isinstance(command, InstallCommand) workers = command.installer._executor._max_workers assert workers == 1 @pytest.mark.parametrize( ("setting",), [ ("installer.no-binary",), ("installer.only-binary",), ], ) @pytest.mark.parametrize( ("value", "expected"), [ ("true", [":all:"]), ("1", [":all:"]), ("false", [":none:"]), ("0", [":none:"]), ("pytest", ["pytest"]), ("PyTest", ["pytest"]), ("pytest,black", ["pytest", "black"]), ("", []), ], ) def test_config_installer_binary_filter_config( tester: CommandTester, setting: str, value: str, expected: list[str] ) -> None: tester.execute(setting) assert tester.io.fetch_output().strip() == "null" config = Config.create() assert not config.get(setting) tester.execute(f"{setting} '{value}'") config = Config.create(reload=True) assert not DeepDiff(config.get(setting), expected, ignore_order=True) def test_config_solver_lazy_wheel( tester: CommandTester, command_tester_factory: CommandTesterFactory ) -> None: tester.execute("--local solver.lazy-wheel") assert tester.io.fetch_output().strip() == "true" repo = LegacyRepository("foo", "https://foo.com") assert repo._lazy_wheel tester.io.clear_output() tester.execute("--local solver.lazy-wheel false") tester.execute("--local solver.lazy-wheel") assert tester.io.fetch_output().strip() == "false" repo = LegacyRepository("foo", "https://foo.com") assert not repo._lazy_wheel current_config = """\ [experimental] system-git-client = true [virtualenvs] prefer-active-python = false """ config_migrated = """\ system-git-client = true [virtualenvs] use-poetry-python = true """ @pytest.mark.parametrize( ["proceed", "expected_config"], [ ("yes", config_migrated), ("no", current_config), ], ) def test_config_migrate( proceed: str, expected_config: str, tester: CommandTester, mocker: MockerFixture, tmp_path: Path, ) -> None: config_dir = tmp_path / "config" mocker.patch("poetry.locations.CONFIG_DIR", config_dir) config_file = Path(config_dir / "config.toml") with config_file.open("w", encoding="utf-8") as fh: fh.write(current_config) tester.execute("--migrate", inputs=proceed) expected_output = textwrap.dedent("""\ Checking for required migrations ... experimental.system-git-client = true -> system-git-client = true virtualenvs.prefer-active-python = false -> virtualenvs.use-poetry-python = true """) output = tester.io.fetch_output() assert output.startswith(expected_output) with config_file.open("r", encoding="utf-8") as fh: assert fh.read() == expected_config def test_config_migrate_local_config(tester: CommandTester, poetry: Poetry) -> None: local_config = poetry.file.path.parent / "poetry.toml" config_data = textwrap.dedent("""\ [experimental] system-git-client = true [virtualenvs] prefer-active-python = false """) with local_config.open("w", encoding="utf-8") as fh: fh.write(config_data) tester.execute("--migrate --local", inputs="yes") expected_config = textwrap.dedent("""\ system-git-client = true [virtualenvs] use-poetry-python = true """) with local_config.open("r", encoding="utf-8") as fh: assert fh.read() == expected_config def test_config_migrate_local_config_should_raise_if_not_found( tester: CommandTester, ) -> None: with pytest.raises(RuntimeError, match="No local config file found"): tester.execute("--migrate --local", inputs="yes") def test_config_installer_build_config_settings( tester: CommandTester, config: Config ) -> None: config_key = "installer.build-config-settings.demo" value = {"CC": "gcc", "--build-option": ["--one", "--two"]} tester.execute(f"{config_key} '{json.dumps(value)}'") assert not DeepDiff(config.config_source.get_property(config_key), value) value_two = {"CC": "g++"} tester.execute(f"{config_key} '{json.dumps(value_two)}'") assert not DeepDiff( config.config_source.get_property(config_key), {**value, **value_two} ) value_three = { "--build-option": ["--three", "--four"], "--package-option": ["--name=foo"], } tester.execute(f"{config_key} '{json.dumps(value_three)}'") assert not DeepDiff( config.config_source.get_property(config_key), { **value, **value_two, **value_three, }, ) tester.execute(f"{config_key} --unset") with pytest.raises(PropertyNotFoundError): config.config_source.get_property(config_key) @pytest.mark.parametrize( "value", [ "BAD=VALUE", "BAD", json.dumps({"key": ["str", 0]}), ], ) def test_config_installer_build_config_settings_bad_values( value: str, tester: CommandTester ) -> None: config_key = "installer.build-config-settings.demo" with pytest.raises(ValueError) as e: tester.execute(f"{config_key} '{value}'") assert str(e.value) == ( f"Invalid build config setting '{value}'. " f"It must be a valid JSON with each property " f"a string or a list of strings." ) def test_command_config_build_config_settings_get( tester: CommandTester, config: Config ) -> None: setting_group = "installer.build-config-settings" setting = f"{setting_group}.foo" # test when no values are configured tester.execute(setting) assert tester.io.fetch_error() == "" assert ( tester.io.fetch_output().strip() == "No build config settings configured for foo." ) tester.execute(setting_group) assert tester.io.fetch_error() == "" assert ( tester.io.fetch_output().strip() == "No packages configured with build config settings." ) # test with one value configured value = {"CC": "gcc", "--build-options": ["--one", "--two"]} tester.execute(f"{setting} '{json.dumps(value)}'") assert tester.status_code == 0 assert tester.io.fetch_output() == tester.io.fetch_error() == "" tester.execute(setting) assert tester.io.fetch_error() == "" assert tester.io.fetch_output().strip() == json.dumps(value) tester.execute(setting_group) assert tester.io.fetch_error() == "" assert tester.io.fetch_output().strip().splitlines() == [ 'foo.--build-options = "[--one, --two]"', 'foo.CC = "gcc"', ] # test getting un-configured value tester.execute(f"{setting_group}.bar") assert tester.io.fetch_error() == "" assert ( tester.io.fetch_output().strip() == "No build config settings configured for bar." ) ================================================ FILE: tests/console/commands/test_init.py ================================================ from __future__ import annotations import os import shutil import sys import textwrap from pathlib import Path from typing import TYPE_CHECKING import pytest from cleo.testers.command_tester import CommandTester from packaging.utils import canonicalize_name from poetry.core.utils.helpers import module_name from poetry.console.application import Application from poetry.console.commands.init import InitCommand from poetry.repositories import RepositoryPool from tests.helpers import get_package if TYPE_CHECKING: from collections.abc import Iterator from unittest.mock import MagicMock from poetry.core.packages.package import Package from pytest import FixtureRequest from pytest_mock import MockerFixture from poetry.config.config import Config from poetry.poetry import Poetry from tests.helpers import PoetryTestApplication from tests.helpers import TestRepository from tests.types import FixtureDirGetter from tests.types import MockedPythonRegister @pytest.fixture def source_dir(tmp_path: Path) -> Iterator[Path]: cwd = Path.cwd() try: os.chdir(tmp_path) yield tmp_path finally: os.chdir(cwd) @pytest.fixture def patches(mocker: MockerFixture, source_dir: Path, repo: TestRepository) -> None: mocker.patch("pathlib.Path.cwd", return_value=source_dir) mocker.patch( "poetry.console.commands.init.InitCommand._get_pool", return_value=RepositoryPool([repo]), ) @pytest.fixture def tester(patches: None) -> CommandTester: app = Application() return CommandTester(app.find("init")) def test_basic_interactive( tester: CommandTester, init_basic_inputs: str, init_basic_toml_no_readme: str ) -> None: tester.execute(inputs=init_basic_inputs) assert init_basic_toml_no_readme in tester.io.fetch_output() def test_noninteractive( app: PoetryTestApplication, mocker: MockerFixture, poetry: Poetry, repo: TestRepository, tmp_path: Path, ) -> None: command = app.find("init") assert isinstance(command, InitCommand) command._pool = poetry.pool repo.add_package(get_package("pytest", "3.6.0")) p = mocker.patch("pathlib.Path.cwd") p.return_value = tmp_path tester = CommandTester(command) args = "--name my-package --dependency pytest" tester.execute(args=args, interactive=False) expected = "Using version ^3.6.0 for pytest\n" assert tester.io.fetch_output() == expected assert tester.io.fetch_error() == "" toml_content = (tmp_path / "pyproject.toml").read_text(encoding="utf-8") assert 'name = "my-package"' in toml_content assert '"pytest (>=3.6.0,<4.0.0)"' in toml_content expected_build_system = textwrap.dedent(""" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" """) assert expected_build_system in toml_content def test_interactive_with_dependencies( tester: CommandTester, repo: TestRepository ) -> None: repo.add_package(get_package("django-pendulum", "0.1.6-pre4")) repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) repo.add_package(get_package("flask", "2.0.0")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "pendulu", # Search for package "1", # Second option is pendulum "", # Do not set constraint "Flask", "0", "", "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package "0", "", "", "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" dependencies = [ "pendulum (>=2.0.0,<3.0.0)", "flask (>=2.0.0,<3.0.0)" ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" """ assert expected in tester.io.fetch_output() # Regression test for https://github.com/python-poetry/poetry/issues/2355 def test_interactive_with_dependencies_and_no_selection( tester: CommandTester, repo: TestRepository ) -> None: repo.add_package(get_package("django-pendulum", "0.1.6-pre4")) repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "pendulu", # Search for package "", # Do not select an option "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package "", # Do not select an option "", "", "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" """ assert expected in tester.io.fetch_output() def test_empty_license(tester: CommandTester) -> None: inputs = [ "my-package", # Package name "1.2.3", # Version "", # Description "n", # Author "", # License "", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) python = ".".join(str(c) for c in sys.version_info[:2]) expected = f"""\ [project] name = "my-package" version = "1.2.3" description = "" authors = [ {{name = "Your Name",email = "you@example.com"}} ] requires-python = ">={python}" """ assert expected in tester.io.fetch_output() def test_interactive_with_git_dependencies( tester: CommandTester, repo: TestRepository ) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/demo.git", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package "0", "", "", "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" dependencies = [ "demo @ git+https://github.com/demo/demo.git" ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] """ assert expected in tester.io.fetch_output() _generate_choice_list_packages_params: list[list[Package]] = [ [ get_package("flask-blacklist", "1.0.0"), get_package("Flask-Shelve", "1.0.0"), get_package("flask-pwa", "1.0.0"), get_package("Flask-test1", "1.0.0"), get_package("Flask-test2", "1.0.0"), get_package("Flask-test3", "1.0.0"), get_package("Flask-test4", "1.0.0"), get_package("Flask-test5", "1.0.0"), get_package("Flask", "1.0.0"), get_package("Flask-test6", "1.0.0"), get_package("Flask-test7", "1.0.0"), ], [ get_package("flask-blacklist", "1.0.0"), get_package("Flask-Shelve", "1.0.0"), get_package("flask-pwa", "1.0.0"), get_package("Flask-test1", "1.0.0"), get_package("Flask", "1.0.0"), ], ] @pytest.fixture(params=_generate_choice_list_packages_params) def _generate_choice_list_packages(request: FixtureRequest) -> list[Package]: packages: list[Package] = request.param return packages @pytest.mark.parametrize("package_name", ["flask", "Flask", "flAsK"]) def test_generate_choice_list( tester: CommandTester, package_name: str, _generate_choice_list_packages: list[Package], ) -> None: init_command = tester.command assert isinstance(init_command, InitCommand) packages = _generate_choice_list_packages choices = init_command._generate_choice_list( packages, canonicalize_name(package_name) ) assert choices[0] == "Flask" def test_interactive_with_git_dependencies_with_reference( tester: CommandTester, repo: TestRepository ) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/demo.git@develop", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package "0", "", "", "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" dependencies = [ "demo @ git+https://github.com/demo/demo.git@develop" ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] """ assert expected in tester.io.fetch_output() def test_interactive_with_git_dependencies_and_other_name( tester: CommandTester, repo: TestRepository ) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/pyproject-demo.git", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package "0", "", "", "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" dependencies = [ "demo @ git+https://github.com/demo/pyproject-demo.git" ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] """ assert expected in tester.io.fetch_output() def test_interactive_with_directory_dependency( tester: CommandTester, repo: TestRepository, source_dir: Path, fixture_dir: FixtureDirGetter, ) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) demo = fixture_dir("git") / "github.com" / "demo" / "demo" shutil.copytree(str(demo), str(source_dir / "demo")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "./demo", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package "0", "", "", "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) demo_uri = (Path.cwd() / "demo").as_uri() expected = f"""\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {{name = "Your Name",email = "you@example.com"}} ] license = {{text = "MIT"}} requires-python = ">=3.6" dependencies = [ "demo @ {demo_uri}" ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] """ assert expected in tester.io.fetch_output() def test_interactive_with_directory_dependency_and_other_name( tester: CommandTester, repo: TestRepository, source_dir: Path, fixture_dir: FixtureDirGetter, ) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) demo = fixture_dir("git") / "github.com" / "demo" / "pyproject-demo" shutil.copytree(str(demo), str(source_dir / "pyproject-demo")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "./pyproject-demo", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package "0", "", "", "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) demo_uri = (Path.cwd() / "pyproject-demo").as_uri() expected = f"""\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {{name = "Your Name",email = "you@example.com"}} ] license = {{text = "MIT"}} requires-python = ">=3.6" dependencies = [ "demo @ {demo_uri}" ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] """ assert expected in tester.io.fetch_output() def test_interactive_with_file_dependency( tester: CommandTester, repo: TestRepository, source_dir: Path, fixture_dir: FixtureDirGetter, ) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) demo = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" shutil.copyfile(str(demo), str(source_dir / demo.name)) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "./demo-0.1.0-py2.py3-none-any.whl", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package "0", "", "", "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) demo_uri = (Path.cwd() / "demo-0.1.0-py2.py3-none-any.whl").as_uri() expected = f"""\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {{name = "Your Name",email = "you@example.com"}} ] license = {{text = "MIT"}} requires-python = ">=3.6" dependencies = [ "demo @ {demo_uri}" ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] """ assert expected in tester.io.fetch_output() def test_interactive_with_wrong_dependency_inputs( tester: CommandTester, repo: TestRepository ) -> None: inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.8", # Python "", # Interactive packages "foo 1.19.2", "pendulum 2.0.0 foo", # Package name and constraint (invalid) "pendulum@^2.0.0", # Package name and constraint (valid) "", # End package selection "", # Interactive dev packages "pytest 3.6.0 foo", # Dev package name and constraint (invalid) "pytest 3.6.0", # Dev package name and constraint (invalid) "pytest@3.6.0", # Dev package name and constraint (valid) "", # End package selection "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.8" dependencies = [ "foo (==1.19.2)", "pendulum (>=2.0.0,<3.0.0)" ] [dependency-groups] dev = [ "pytest (==3.6.0)" ] """ assert expected in tester.io.fetch_output() def test_python_option(tester: CommandTester) -> None: inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ] tester.execute("--python '>=3.6'", inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" """ assert expected in tester.io.fetch_output() def test_predefined_dependency(tester: CommandTester, repo: TestRepository) -> None: repo.add_package(get_package("pendulum", "2.0.0")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ] tester.execute("--dependency pendulum", inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" dependencies = [ "pendulum (>=2.0.0,<3.0.0)" ] """ assert expected in tester.io.fetch_output() def test_predefined_and_interactive_dependencies( tester: CommandTester, repo: TestRepository ) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pyramid", "1.10")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "", # Interactive packages "pyramid", # Search for package "0", # First option "", # Do not set constraint "", # Stop searching for packages "n", # Interactive dev packages "\n", # Generate ] tester.execute("--dependency pendulum", inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" dependencies = [ "pendulum (>=2.0.0,<3.0.0)", "pyramid (>=1.10,<2.0)" ] """ assert expected in tester.io.fetch_output() def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository) -> None: repo.add_package(get_package("pytest", "3.6.0")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ] tester.execute("--dev-dependency pytest", inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" dependencies = [ ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] """ assert expected in tester.io.fetch_output() def test_predefined_and_interactive_dev_dependencies( tester: CommandTester, repo: TestRepository ) -> None: repo.add_package(get_package("pytest", "3.6.0")) repo.add_package(get_package("pytest-requests", "0.2.0")) inputs = [ "my-package", # Package name "1.2.3", # Version "This is a description", # Description "n", # Author "MIT", # License ">=3.6", # Python "n", # Interactive packages "", # Interactive dev packages "pytest-requests", # Search for package "0", # Select first option "", # Do not set constraint "", # Stop searching for dev packages "\n", # Generate ] tester.execute("--dev-dependency pytest", inputs="\n".join(inputs)) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Your Name",email = "you@example.com"} ] license = {text = "MIT"} requires-python = ">=3.6" dependencies = [ ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)", "pytest-requests (>=0.2.0,<0.3.0)" ] """ output = tester.io.fetch_output() assert expected in output def test_predefined_all_options(tester: CommandTester, repo: TestRepository) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) inputs = [ "1.2.3", # Version "", # Author "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ] tester.execute( "--name my-package " "--description 'This is a description' " "--author 'Foo Bar ' " "--python '>=3.8' " "--license MIT " "--dependency pendulum " "--dev-dependency pytest", inputs="\n".join(inputs), ) expected = """\ [project] name = "my-package" version = "1.2.3" description = "This is a description" authors = [ {name = "Foo Bar",email = "foo@example.com"} ] license = {text = "MIT"} requires-python = ">=3.8" dependencies = [ "pendulum (>=2.0.0,<3.0.0)" ] [dependency-groups] dev = [ "pytest (>=3.6.0,<4.0.0)" ] """ output = tester.io.fetch_output() assert expected in output def test_add_package_with_extras_and_whitespace(tester: CommandTester) -> None: command = tester.command assert isinstance(command, InitCommand) result = command._parse_requirements(["databases[postgresql, sqlite]"]) assert result[0]["name"] == "databases" assert len(result[0]["extras"]) == 2 assert "postgresql" in result[0]["extras"] assert "sqlite" in result[0]["extras"] def test_init_existing_pyproject_simple( tester: CommandTester, source_dir: Path, init_basic_inputs: str, init_basic_toml_no_readme: str, ) -> None: pyproject_file = source_dir / "pyproject.toml" existing_section = """ [tool.black] line-length = 88 """ pyproject_file.write_text(existing_section, encoding="utf-8") tester.execute(inputs=init_basic_inputs) assert ( f"{existing_section}\n{init_basic_toml_no_readme}" in pyproject_file.read_text(encoding="utf-8") ) @pytest.mark.parametrize("linesep", ["\n", "\r\n"]) def test_init_existing_pyproject_consistent_linesep( tester: CommandTester, source_dir: Path, init_basic_inputs: str, init_basic_toml_no_readme: str, linesep: str, ) -> None: pyproject_file = source_dir / "pyproject.toml" existing_section = """ [tool.black] line-length = 88 """.replace("\n", linesep) with open(pyproject_file, "w", newline="", encoding="utf-8") as f: f.write(existing_section) tester.execute(inputs=init_basic_inputs) with open(pyproject_file, newline="", encoding="utf-8") as f: content = f.read() init_basic_toml = init_basic_toml_no_readme.replace("\n", linesep) assert f"{existing_section}{linesep}{init_basic_toml}" in content def test_init_non_interactive_existing_pyproject_add_dependency( tester: CommandTester, source_dir: Path, init_basic_inputs: str, repo: TestRepository, ) -> None: pyproject_file = source_dir / "pyproject.toml" existing_section = """ [tool.black] line-length = 88 """ pyproject_file.write_text(existing_section, encoding="utf-8") repo.add_package(get_package("foo", "1.19.2")) tester.execute( "--author 'Your Name ' " "--name 'my-package' " "--python '>=3.6' " "--dependency foo", interactive=False, ) expected = """\ [project] name = "my-package" version = "0.1.0" description = "" authors = [ {name = "Your Name",email = "you@example.com"} ] requires-python = ">=3.6" dependencies = [ "foo (>=1.19.2,<2.0.0)" ] """ assert f"{existing_section}\n{expected}" in pyproject_file.read_text( encoding="utf-8" ) def test_init_existing_pyproject_with_build_system_fails( tester: CommandTester, source_dir: Path, init_basic_inputs: str ) -> None: pyproject_file = source_dir / "pyproject.toml" existing_section = """ [build-system] requires = ["setuptools >= 40.6.0", "wheel"] build-backend = "setuptools.build_meta" """ pyproject_file.write_text(existing_section, encoding="utf-8") tester.execute(inputs=init_basic_inputs) assert ( tester.io.fetch_error().strip() == "A pyproject.toml file with a defined build-system already exists." ) assert existing_section in pyproject_file.read_text(encoding="utf-8") @pytest.mark.parametrize( "name", [ None, "", "foo", " foo ", "foo==2.0", "foo@2.0", " foo@2.0 ", "foo 2.0", " foo 2.0 ", ], ) def test_validate_package_valid(name: str | None) -> None: assert InitCommand._validate_package(name) == name @pytest.mark.parametrize( "name", ["foo bar 2.0", " foo bar 2.0 ", "foo bar foobar 2.0"] ) def test_validate_package_invalid(name: str) -> None: with pytest.raises(ValueError): assert InitCommand._validate_package(name) @pytest.mark.parametrize( "author", [ str(b"Jos\x65\xcc\x81 Duarte", "utf-8"), str(b"Jos\xc3\xa9 Duarte", "utf-8"), ], ) def test_validate_author(author: str) -> None: """ This test was added following issue #8779, hence, we're looking to see if the test no longer throws an exception, hence the seemingly "useless" test of just running the method. """ InitCommand._validate_author(author, "") @pytest.mark.parametrize( "package_name, include", ( ("mypackage", None), ("my-package", "my_package"), ("my.package", "my"), ("my-awesome-package", "my_awesome_package"), ("my.awesome.package", "my"), ), ) def test_package_include( tester: CommandTester, package_name: str, include: str | None, ) -> None: tester.execute( inputs="\n".join( ( package_name, "", # Version "", # Description "poetry", # Author "", # License ">=3.10", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ), ), ) packages = "" if include and module_name(package_name) != include: packages = f'\n[tool.poetry]\npackages = [{{include = "{include}"}}]\n' expected = ( "[project]\n" f'name = "{package_name.replace(".", "-")}"\n' # canonicalized 'version = "0.1.0"\n' 'description = ""\n' "authors = [\n" ' {name = "poetry"}\n' "]\n" 'requires-python = ">=3.10"\n' "dependencies = [\n" "]\n" f"{packages}" # This line is optional. Thus, no newline here. ) assert expected in tester.io.fetch_output() @pytest.mark.parametrize( ["use_poetry_python", "python"], [ (False, "1.1"), (True, f"{sys.version_info[0]}.{sys.version_info[1]}"), ], ) def test_respect_use_poetry_python_on_init( use_poetry_python: bool, python: str, config: Config, tester: CommandTester, source_dir: Path, mocked_python_register: MockedPythonRegister, with_no_active_python: MagicMock, ) -> None: mocked_python_register(f"{python}.1", make_system=True) config.config["virtualenvs"]["use-poetry-python"] = use_poetry_python pyproject_file = source_dir / "pyproject.toml" tester.execute( "--author 'Your Name ' --name 'my-package'", interactive=False, ) expected = f"""\ requires-python = ">={python}" """ assert expected in pyproject_file.read_text(encoding="utf-8") def test_get_pool(mocker: MockerFixture, source_dir: Path) -> None: """ Since we are mocking _get_pool() in the other tests, we at least should make sure it works in general. See https://github.com/python-poetry/poetry/issues/8634. """ mocker.patch("pathlib.Path.cwd", return_value=source_dir) app = Application() command = app.find("init") assert isinstance(command, InitCommand) pool = command._get_pool() assert pool.repositories def test_init_does_not_create_project_structure_in_empty_directory( tester: CommandTester, source_dir: Path ) -> None: """Test that poetry init does not create project structure in empty directory.""" inputs = [ "my-package", # Package name "1.0.0", # Version "", # Description "n", # Author "", # License "", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) # Should only create pyproject.toml assert (source_dir / "pyproject.toml").exists() # Should NOT create these assert not (source_dir / "tests").exists() assert not (source_dir / "my_package").exists() assert not (source_dir / "src").exists() assert not (source_dir / "README.md").exists() def test_init_does_not_create_project_structure_in_non_empty_directory( tester: CommandTester, source_dir: Path ) -> None: """Test that poetry init does not create project structure in non-empty directory.""" # Create some existing files (source_dir / "existing_file.txt").write_text("existing content", encoding="utf-8") (source_dir / "existing_dir").mkdir() inputs = [ "my-package", # Package name "1.0.0", # Version "", # Description "n", # Author "", # License "", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate ] tester.execute(inputs="\n".join(inputs)) # Should only create pyproject.toml assert (source_dir / "pyproject.toml").exists() # Should NOT create these assert not (source_dir / "tests").exists() assert not (source_dir / "my_package").exists() assert not (source_dir / "src").exists() assert not (source_dir / "README.md").exists() # Existing files should remain assert (source_dir / "existing_file.txt").exists() assert (source_dir / "existing_dir").exists() def test_init_adds_readme_key_when_readme_exists( tester: CommandTester, tmp_path: Path ) -> None: # Arrange: ensure README.md exists readme = tmp_path / "README.md" readme.write_text("# My Project\n", encoding="utf-8") # Act tester.execute(interactive=False) # Assert pyproject = (tmp_path / "pyproject.toml").read_text(encoding="utf-8") assert 'readme = "README.md"' in pyproject def test_init_does_not_add_readme_key_when_readme_missing( tester: CommandTester, tmp_path: Path ) -> None: # Arrange: ensure README.md does NOT exist readme = tmp_path / "README.md" assert not readme.exists() # Act tester.execute(interactive=False) # Assert pyproject = (tmp_path / "pyproject.toml").read_text(encoding="utf-8") assert "readme =" not in pyproject ================================================ FILE: tests/console/commands/test_install.py ================================================ from __future__ import annotations import re from typing import TYPE_CHECKING import pytest from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.console.commands.installer_command import InstallerCommand from poetry.console.exceptions import GroupNotFoundError from tests.helpers import TestLocker if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.poetry import Poetry from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory PYPROJECT_CONTENT = """\ [tool.poetry] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ "Python Poetry " ] license = "MIT" [tool.poetry.dependencies] python = "~2.7 || ^3.4" fizz = { version = "^1.0", optional = true } buzz = { version = "^2.0", optional = true } [tool.poetry.group.foo.dependencies] foo = "^1.0" [tool.poetry.group.bar.dependencies] bar = "^1.1" [tool.poetry.group.baz.dependencies] baz = "^1.2" [tool.poetry.group.bim.dependencies] bim = "^1.3" [tool.poetry.group.bam] optional = true [tool.poetry.group.bam.dependencies] bam = "^1.4" [tool.poetry.extras] extras_a = [ "fizz" ] extras_b = [ "buzz" ] """ @pytest.fixture def command() -> str: return "install" @pytest.fixture def poetry(project_factory: ProjectFactory) -> Poetry: return project_factory(name="export", pyproject_content=PYPROJECT_CONTENT) @pytest.fixture def tester( command_tester_factory: CommandTesterFactory, command: str, poetry: Poetry ) -> CommandTester: return command_tester_factory(command) def _project_factory( fixture_name: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, ) -> Poetry: source = fixture_dir(fixture_name) pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") poetry_lock_content = (source / "poetry.lock").read_text(encoding="utf-8") return project_factory( name="foobar", pyproject_content=pyproject_content, poetry_lock_content=poetry_lock_content, source=source, ) @pytest.mark.parametrize( ("options", "groups"), [ ("", {MAIN_GROUP, "foo", "bar", "baz", "bim"}), ("--only-root", set()), (f"--only {MAIN_GROUP}", {MAIN_GROUP}), ("--only foo", {"foo"}), ("--only foo,bar", {"foo", "bar"}), ("--only bam", {"bam"}), ("--with bam", {MAIN_GROUP, "foo", "bar", "baz", "bim", "bam"}), ("--without foo,bar", {MAIN_GROUP, "baz", "bim"}), (f"--without {MAIN_GROUP}", {"foo", "bar", "baz", "bim"}), ("--with foo,bar --without baz --without bim --only bam", {"bam"}), ("--all-groups", {MAIN_GROUP, "foo", "bar", "baz", "bim", "bam"}), # net result zero options ("--with foo", {MAIN_GROUP, "foo", "bar", "baz", "bim"}), ("--without bam", {MAIN_GROUP, "foo", "bar", "baz", "bim"}), ("--with bam --without bam", {MAIN_GROUP, "foo", "bar", "baz", "bim"}), ("--with foo --without foo", {MAIN_GROUP, "bar", "baz", "bim"}), ], ) @pytest.mark.parametrize("with_root", [True, False]) def test_group_options_are_passed_to_the_installer( options: str, groups: set[str], with_root: bool, tester: CommandTester, mocker: MockerFixture, ) -> None: """ Group options are passed properly to the installer. """ assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) editable_builder_mock = mocker.patch( "poetry.masonry.builders.editable.EditableBuilder", side_effect=ModuleOrPackageNotFoundError(), ) if not with_root: options = f"--no-root {options}" status_code = tester.execute(options) if options == "--no-root --only-root" or with_root: assert status_code == 1 return else: assert status_code == 0 package_groups = set(tester.command.poetry.package._dependency_groups) installer_groups = set(tester.command.installer._groups or []) assert installer_groups <= package_groups assert set(installer_groups) == groups if with_root: assert editable_builder_mock.call_count == 1 assert editable_builder_mock.call_args_list[0][0][0] == tester.command.poetry else: assert editable_builder_mock.call_count == 0 @pytest.mark.parametrize("sync", [True, False]) def test_sync_option_is_passed_to_the_installer( tester: CommandTester, mocker: MockerFixture, sync: bool ) -> None: """ The --sync option is passed properly to the installer. """ assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) tester.execute("--sync" if sync else "") assert tester.command.installer._requires_synchronization is sync error = tester.io.fetch_error() if sync: assert "deprecated" in error assert "poetry sync" in error else: assert error == "" @pytest.mark.parametrize("compile", [False, True]) def test_compile_option_is_passed_to_the_installer( tester: CommandTester, mocker: MockerFixture, compile: bool ) -> None: """ The --compile option is passed properly to the installer. """ assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) enable_bytecode_compilation_mock = mocker.patch.object( tester.command.installer.executor._wheel_installer, "enable_bytecode_compilation", ) tester.execute("--compile" if compile else "") enable_bytecode_compilation_mock.assert_called_once_with(compile) @pytest.mark.parametrize("skip_directory_cli_value", [True, False]) def test_no_directory_is_passed_to_installer( tester: CommandTester, mocker: MockerFixture, skip_directory_cli_value: bool ) -> None: """ The --no-directory option is passed to the installer. """ assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) if skip_directory_cli_value is True: tester.execute("--no-directory") else: tester.execute() assert tester.command.installer._skip_directory is skip_directory_cli_value def test_no_all_extras_doesnt_populate_installer( tester: CommandTester, mocker: MockerFixture ) -> None: """ Not passing --all-extras means the installer doesn't see any extras. """ assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) tester.execute() assert not tester.command.installer._extras def test_all_extras_populates_installer( tester: CommandTester, mocker: MockerFixture ) -> None: """ The --all-extras option results in extras passed to the installer. """ assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) tester.execute("--all-extras") assert tester.command.installer._extras == ["extras-a", "extras-b"] def test_extras_are_parsed_and_populate_installer( tester: CommandTester, mocker: MockerFixture, ) -> None: assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) tester.execute('--extras "first second third"') assert tester.command.installer._extras == ["first", "second", "third"] @pytest.mark.parametrize( ("options", "call_count"), [ ("--no-plugins", 0), ("", 1), ], ) def test_install_ensures_project_plugins( tester: CommandTester, mocker: MockerFixture, options: str, call_count: int ) -> None: assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) ensure_project_plugins = mocker.patch( "poetry.plugins.plugin_manager.PluginManager.ensure_project_plugins" ) tester.execute(options) assert ensure_project_plugins.call_count == call_count def test_extras_conflicts_all_extras( tester: CommandTester, mocker: MockerFixture ) -> None: """ The --extras doesn't make sense with --all-extras. """ assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) tester.execute("--extras foo --all-extras") assert tester.status_code == 1 assert ( tester.io.fetch_error() == "You cannot specify explicit `--extras` while installing using" " `--all-extras`.\n" ) @pytest.mark.parametrize( "options", [ "--with foo", "--without foo", "--with foo,bar --without baz", "--only foo", "--all-groups", ], ) def test_only_root_conflicts_with_without_only_all_groups( options: str, tester: CommandTester, mocker: MockerFixture, ) -> None: assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) tester.execute(f"{options} --only-root") assert tester.status_code == 1 assert ( tester.io.fetch_error() == "The `--with`, `--without`, `--only` and `--all-groups` options cannot be used with" " the `--only-root` option.\n" ) @pytest.mark.parametrize( "options", [ "--with foo", "--without foo", "--with foo,bar --without baz", "--only foo", ], ) def test_all_groups_conflicts_with_only_with_without( options: str, tester: CommandTester, mocker: MockerFixture, ) -> None: assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) tester.execute(f"{options} --all-groups") assert tester.status_code == 1 assert ( tester.io.fetch_error() == "You cannot specify `--with`, `--without`, or `--only` when using `--all-groups`.\n" ) @pytest.mark.parametrize( ("options", "valid_groups", "should_raise"), [ ({"--with": MAIN_GROUP}, {MAIN_GROUP}, False), ({"--with": "spam"}, set(), True), ({"--with": "spam,foo"}, {"foo"}, True), ({"--without": "spam"}, set(), True), ({"--without": "spam,bar"}, {"bar"}, True), ({"--with": "eggs,ham", "--without": "spam"}, set(), True), ({"--with": "eggs,ham", "--without": "spam,baz"}, {"baz"}, True), ({"--only": "spam"}, set(), True), ({"--only": "bim"}, {"bim"}, False), ({"--only": MAIN_GROUP}, {MAIN_GROUP}, False), ], ) def test_invalid_groups_with_without_only( tester: CommandTester, mocker: MockerFixture, options: dict[str, str], valid_groups: set[str], should_raise: bool, ) -> None: assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) cmd_args = " ".join(f"{flag} {groups}" for (flag, groups) in options.items()) if not should_raise: tester.execute(cmd_args) assert tester.status_code == 1 else: with pytest.raises(GroupNotFoundError, match=r"^Group\(s\) not found:") as e: tester.execute(cmd_args) assert tester.status_code is None for opt, groups in options.items(): group_list = groups.split(",") invalid_groups = sorted(set(group_list) - valid_groups) for group in invalid_groups: assert ( re.search(rf"{group} \(via .*{opt}.*\)", str(e.value)) is not None ) def test_dry_run_populates_installer( tester: CommandTester, mocker: MockerFixture ) -> None: """ The --dry-run option results in extras passed to the installer. """ assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) tester.execute("--dry-run") assert tester.command.installer._dry_run is True def test_dry_run_does_not_build(tester: CommandTester, mocker: MockerFixture) -> None: assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) mocked_editable_builder = mocker.patch( "poetry.masonry.builders.editable.EditableBuilder" ) tester.execute("--dry-run") assert mocked_editable_builder.return_value.build.call_count == 0 def test_install_logs_output(tester: CommandTester, mocker: MockerFixture) -> None: assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) mocker.patch("poetry.masonry.builders.editable.EditableBuilder") tester.execute() assert tester.status_code == 0 assert ( tester.io.fetch_output() == "\nInstalling the current project: simple-project (1.2.3)\n" ) def test_install_logs_output_decorated( tester: CommandTester, mocker: MockerFixture ) -> None: assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) mocker.patch("poetry.masonry.builders.editable.EditableBuilder") tester.execute(decorated=True) expected = ( "\n" "\x1b[39;1mInstalling\x1b[39;22m the current project: " "\x1b[36msimple-project\x1b[39m (\x1b[39;1m1.2.3\x1b[39;22m)" "\x1b[1G\x1b[2K" "\x1b[39;1mInstalling\x1b[39;22m the current project: " "\x1b[36msimple-project\x1b[39m (\x1b[32m1.2.3\x1b[39m)" "\n" ) assert tester.status_code == 0 assert tester.io.fetch_output() == expected @pytest.mark.parametrize("with_root", [True, False]) @pytest.mark.parametrize("error", ["module", "readme", ""]) def test_install_warning_corrupt_root( command_tester_factory: CommandTesterFactory, command: str, project_factory: ProjectFactory, with_root: bool, error: str, ) -> None: name = "corrupt" content = f"""\ [tool.poetry] name = "{name}" version = "1.2.3" description = "" authors = [] """ if error == "readme": content += 'readme = "missing_readme.md"\n' poetry = project_factory(name=name, pyproject_content=content) if error != "module": (poetry.pyproject_path.parent / f"{name}.py").touch() tester = command_tester_factory(command, poetry=poetry) tester.execute("" if with_root else "--no-root") if error and with_root: assert tester.status_code == 1 else: assert tester.status_code == 0 if with_root and error: assert "The current project could not be installed: " in tester.io.fetch_error() else: assert tester.io.fetch_error() == "" @pytest.mark.parametrize("options", ["", "--without dev"]) @pytest.mark.parametrize( "project", ["missing_directory_dependency", "missing_file_dependency"] ) def test_install_path_dependency_does_not_exist( command_tester_factory: CommandTesterFactory, command: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, project: str, options: str, ) -> None: poetry = _project_factory(project, project_factory, fixture_dir) assert isinstance(poetry.locker, TestLocker) poetry.locker.locked(True) tester = command_tester_factory(command, poetry=poetry) if options: tester.execute(options) else: with pytest.raises(ValueError, match="does not exist"): tester.execute(options) @pytest.mark.parametrize("options", ["", "--extras notinstallable"]) def test_install_extra_path_dependency_does_not_exist( command_tester_factory: CommandTesterFactory, command: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, options: str, ) -> None: project = "missing_extra_directory_dependency" poetry = _project_factory(project, project_factory, fixture_dir) assert isinstance(poetry.locker, TestLocker) poetry.locker.locked(True) tester = command_tester_factory(command, poetry=poetry) if not options: tester.execute(options) else: with pytest.raises(ValueError, match="does not exist"): tester.execute(options) @pytest.mark.parametrize("options", ["", "--no-directory"]) def test_install_missing_directory_dependency_with_no_directory( command_tester_factory: CommandTesterFactory, command: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, options: str, ) -> None: poetry = _project_factory( "missing_directory_dependency", project_factory, fixture_dir ) assert isinstance(poetry.locker, TestLocker) poetry.locker.locked(True) tester = command_tester_factory(command, poetry=poetry) if options: tester.execute(options) else: with pytest.raises(ValueError, match="does not exist"): tester.execute(options) def test_non_package_mode_does_not_try_to_install_root( command_tester_factory: CommandTesterFactory, command: str, project_factory: ProjectFactory, ) -> None: content = """\ [tool.poetry] package-mode = false """ poetry = project_factory(name="non-package-mode", pyproject_content=content) tester = command_tester_factory(command, poetry=poetry) tester.execute() assert tester.status_code == 0 assert tester.io.fetch_error() == "" ================================================ FILE: tests/console/commands/test_lock.py ================================================ from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING import pytest from poetry.core.constraints.version import Version from poetry.packages import Locker from tests.helpers import get_package if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from poetry.poetry import Poetry from tests.helpers import TestRepository from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture def source_dir(tmp_path: Path) -> Path: return Path(tmp_path.as_posix()) @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("lock") def _project_factory( fixture_name: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, ) -> Poetry: source = fixture_dir(fixture_name) pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") poetry_lock_content = (source / "poetry.lock").read_text(encoding="utf-8") return project_factory( name="foobar", pyproject_content=pyproject_content, poetry_lock_content=poetry_lock_content, source=source, ) @pytest.fixture def poetry_with_outdated_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: return _project_factory("outdated_lock", project_factory, fixture_dir) @pytest.fixture def poetry_with_up_to_date_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: return _project_factory("up_to_date_lock", project_factory, fixture_dir) @pytest.fixture def poetry_with_old_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: return _project_factory("old_lock", project_factory, fixture_dir) @pytest.fixture def poetry_with_nested_path_deps_old_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: return _project_factory("old_lock_path_dependency", project_factory, fixture_dir) @pytest.fixture def poetry_with_incompatible_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: return _project_factory("incompatible_lock", project_factory, fixture_dir) @pytest.fixture def poetry_with_invalid_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: return _project_factory("invalid_lock", project_factory, fixture_dir) def test_lock_does_not_update_if_not_necessary( command_tester_factory: CommandTesterFactory, poetry_with_old_lockfile: Poetry, repo: TestRepository, ) -> None: package = get_package("sampleproject", "1.3.1") repo.add_package(package) repo.add_package(get_package("sampleproject", "2.0.0")) locker = Locker( lock=poetry_with_old_lockfile.pyproject.file.path.parent / "poetry.lock", pyproject_data=poetry_with_old_lockfile.locker._pyproject_data, ) poetry_with_old_lockfile.set_locker(locker) locked_repository = poetry_with_old_lockfile.locker.locked_repository() assert ( poetry_with_old_lockfile.locker.lock_data["metadata"].get("lock-version") == "1.0" ) # set correct files to avoid cache refresh package.files = ( locker.locked_repository() .package("sampleproject", Version.parse("1.3.1")) .files ) tester = command_tester_factory("lock", poetry=poetry_with_old_lockfile) tester.execute() locker = Locker( lock=poetry_with_old_lockfile.pyproject.file.path.parent / "poetry.lock", pyproject_data={}, ) packages = locker.locked_repository().packages assert len(packages) == len(locked_repository.packages) assert locker.lock_data["metadata"].get("lock-version") == "2.1" for package in packages: assert locked_repository.find_packages(package.to_dependency()) @pytest.mark.parametrize("regenerate", [True, False]) def test_lock_always_updates_path_dependencies( command_tester_factory: CommandTesterFactory, poetry_with_nested_path_deps_old_lockfile: Poetry, repo: TestRepository, regenerate: bool, ) -> None: """ The lock file contains a variant of the directory dependency "quix" that does not depend on "sampleproject". Although the version of "quix" has not been changed, it should be re-solved because there is always only one valid version of a directory dependency at any time. """ repo.add_package(get_package("sampleproject", "1.3.1")) locker = Locker( lock=poetry_with_nested_path_deps_old_lockfile.pyproject.file.path.parent / "poetry.lock", pyproject_data=poetry_with_nested_path_deps_old_lockfile.locker._pyproject_data, ) poetry_with_nested_path_deps_old_lockfile.set_locker(locker) tester = command_tester_factory( "lock", poetry=poetry_with_nested_path_deps_old_lockfile ) tester.execute("--regenerate" if regenerate else "") packages = locker.locked_repository().packages assert {p.name for p in packages} == {"quix", "sampleproject"} @pytest.mark.parametrize("regenerate", [True, False]) @pytest.mark.parametrize( "project", ["missing_directory_dependency", "missing_file_dependency"] ) def test_lock_path_dependency_does_not_exist( command_tester_factory: CommandTesterFactory, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, project: str, regenerate: bool, ) -> None: poetry = _project_factory(project, project_factory, fixture_dir) locker = Locker( lock=poetry.pyproject.file.path.parent / "poetry.lock", pyproject_data=poetry.locker._pyproject_data, ) poetry.set_locker(locker) options = "--regenerate" if regenerate else "" tester = command_tester_factory("lock", poetry=poetry) if regenerate or "directory" in project: # directory dependencies are always updated with pytest.raises(ValueError, match="does not exist"): tester.execute(options) else: tester.execute(options) @pytest.mark.parametrize("regenerate", [True, False]) @pytest.mark.parametrize( "project", ["deleted_directory_dependency", "deleted_file_dependency"] ) def test_lock_path_dependency_deleted_from_pyproject( command_tester_factory: CommandTesterFactory, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, project: str, regenerate: bool, ) -> None: poetry = _project_factory(project, project_factory, fixture_dir) locker = Locker( lock=poetry.pyproject.file.path.parent / "poetry.lock", pyproject_data=poetry.locker._pyproject_data, ) poetry.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry) tester.execute("--regenerate" if regenerate else "") packages = locker.locked_repository().packages assert {p.name for p in packages} == set() @pytest.mark.parametrize("regenerate", [True, False]) def test_lock_with_incompatible_lockfile( command_tester_factory: CommandTesterFactory, poetry_with_incompatible_lockfile: Poetry, repo: TestRepository, regenerate: bool, ) -> None: repo.add_package(get_package("sampleproject", "1.3.1")) locker = Locker( lock=poetry_with_incompatible_lockfile.pyproject.file.path.parent / "poetry.lock", pyproject_data=poetry_with_incompatible_lockfile.locker._pyproject_data, ) poetry_with_incompatible_lockfile.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry_with_incompatible_lockfile) if regenerate: # still possible because lock file is not required status_code = tester.execute("--regenerate") assert status_code == 0 else: # not possible because of incompatible lock file expected = ( "(?s)lock file is not compatible .*" " regenerate the lock file with the `poetry lock` command" ) with pytest.raises(RuntimeError, match=expected): tester.execute() @pytest.mark.parametrize("regenerate", [True, False]) def test_lock_with_invalid_lockfile( command_tester_factory: CommandTesterFactory, poetry_with_invalid_lockfile: Poetry, repo: TestRepository, regenerate: bool, ) -> None: repo.add_package(get_package("sampleproject", "1.3.1")) locker = Locker( lock=poetry_with_invalid_lockfile.pyproject.file.path.parent / "poetry.lock", pyproject_data=poetry_with_invalid_lockfile.locker._pyproject_data, ) poetry_with_invalid_lockfile.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry_with_invalid_lockfile) if regenerate: # still possible because lock file is not required status_code = tester.execute("--regenerate") assert status_code == 0 else: # not possible because of broken lock file with pytest.raises(RuntimeError, match="Unable to read the lock file"): tester.execute() ================================================ FILE: tests/console/commands/test_new.py ================================================ from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING import pytest from poetry.core.utils.helpers import module_name from poetry.factory import Factory if TYPE_CHECKING: from unittest.mock import MagicMock from cleo.testers.command_tester import CommandTester from poetry.config.config import Config from poetry.poetry import Poetry from tests.types import CommandTesterFactory from tests.types import MockedPythonRegister @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("new") def verify_project_directory( path: Path, package_name: str, package_path: str | Path, is_flat: bool = False, ) -> Poetry: package_path = Path(package_path) assert path.is_dir() pyproject = path / "pyproject.toml" assert pyproject.is_file() init_file = path / package_path / "__init__.py" assert init_file.is_file() tests_init_file = path / "tests" / "__init__.py" assert tests_init_file.is_file() poetry = Factory().create_poetry(cwd=path) assert poetry.package.name == package_name if is_flat: package_include = {"include": package_path.parts[0]} else: package_include = { "include": package_path.relative_to("src").parts[0], "from": "src", } name = poetry.package.name packages = poetry.local_config.get("packages") if not packages: assert module_name(name) == package_include.get("include") else: assert len(packages) == 1 assert packages[0] == package_include return poetry @pytest.mark.parametrize( "options,directory,package_name,package_path,include_from", [ (["--flat"], "package", "package", "package", None), ([], "package", "package", "src/package", "src"), ( ["--flat", "--name namespace.package"], "namespace-package", "namespace-package", "namespace/package", None, ), ( ["--name namespace.package"], "namespace-package", "namespace-package", "src/namespace/package", "src", ), ( ["--flat", "--name namespace.package_a"], "namespace-package_a", "namespace-package-a", "namespace/package_a", None, ), ( ["--name namespace.package_a"], "namespace-package_a", "namespace-package-a", "src/namespace/package_a", "src", ), ( ["--flat", "--name namespace_package"], "namespace-package", "namespace-package", "namespace_package", None, ), ( ["--name namespace_package"], "namespace-package", "namespace-package", "src/namespace_package", "src", ), ( ["--flat", "--name namespace.package"], "package", "namespace-package", "namespace/package", None, ), ( ["--name namespace.package"], "package", "namespace-package", "src/namespace/package", "src", ), ( ["--name namespace.package", "--flat"], "package", "namespace-package", "namespace/package", None, ), ( ["--name namespace.package"], "package", "namespace-package", "src/namespace/package", "src", ), ( ["--flat"], "namespace_package", "namespace-package", "namespace_package", None, ), ( ["--name namespace_package"], "namespace_package", "namespace-package", "src/namespace_package", "src", ), ], ) def test_command_new( options: list[str], directory: str, package_name: str, package_path: str, include_from: str | None, tester: CommandTester, tmp_path: Path, ) -> None: path = tmp_path / directory options.append(str(path)) tester.execute(" ".join(options)) verify_project_directory(path, package_name, package_path, "--flat" in options) @pytest.mark.parametrize(("fmt",), [(None,), ("md",), ("rst",), ("adoc",), ("creole",)]) def test_command_new_with_readme( fmt: str | None, tester: CommandTester, tmp_path: Path ) -> None: package = "package" path = tmp_path / package options = [path.as_posix()] if fmt: options.insert(0, f"--readme {fmt}") tester.execute(" ".join(options)) poetry = verify_project_directory(path, package, Path("src") / package) project_section = poetry.pyproject.data["project"] assert isinstance(project_section, dict) readme_file = path / f"README.{fmt or 'md'}" assert readme_file.exists() assert "readme" in project_section assert project_section["readme"] == readme_file.name @pytest.mark.parametrize( ["use_poetry_python", "python"], [ (False, "1.1"), (True, f"{sys.version_info[0]}.{sys.version_info[1]}"), ], ) def test_respect_use_poetry_python_on_new( use_poetry_python: bool, python: str, config: Config, tester: CommandTester, tmp_path: Path, mocked_python_register: MockedPythonRegister, with_no_active_python: MagicMock, ) -> None: mocked_python_register(f"{python}.1", make_system=True) config.config["virtualenvs"]["use-poetry-python"] = use_poetry_python package = "package" path = tmp_path / package options = [str(path)] tester.execute(" ".join(options)) pyproject_file = path / "pyproject.toml" expected = f"""\ requires-python = ">={python}" """ assert expected in pyproject_file.read_text(encoding="utf-8") def test_basic_interactive_new( tester: CommandTester, tmp_path: Path, init_basic_inputs: str, new_basic_toml: str ) -> None: path = tmp_path / "somepackage" tester.execute(f"--interactive {path.as_posix()}", inputs=init_basic_inputs) verify_project_directory(path, "my-package", "src/my_package") assert new_basic_toml in tester.io.fetch_output() def test_new_creates_structure_in_empty_existing_directory( tester: CommandTester, tmp_path: Path ) -> None: """Test that poetry new creates structure in existing but empty directory.""" # Create empty directory package_dir = tmp_path / "my-package" package_dir.mkdir() tester.execute(str(package_dir)) # Should create full project structure verify_project_directory(package_dir, "my-package", "src/my_package") assert (package_dir / "tests").exists() assert (package_dir / "src" / "my_package").exists() assert (package_dir / "pyproject.toml").exists() assert (package_dir / "README.md").exists() def test_new_with_dot_in_empty_directory(tester: CommandTester, tmp_path: Path) -> None: """Test that poetry new . works in empty directory and creates structure.""" import os test_dir = "test_new_with_dot_in_empty_directory" # Change to the temporary directory original_cwd = os.getcwd() os.chdir(tmp_path) os.mkdir(test_dir) tmp_path = Path(original_cwd) / test_dir os.chdir(tmp_path) try: tester.execute(".") # Should create full project structure assert (tmp_path / "tests").exists() assert (tmp_path / "src").exists() assert (tmp_path / "pyproject.toml").exists() assert (tmp_path / "README.md").exists() finally: # Always restore original directory os.chdir(original_cwd) ================================================ FILE: tests/console/commands/test_publish.py ================================================ from __future__ import annotations import shutil from pathlib import Path from typing import TYPE_CHECKING from typing import NoReturn import pytest import requests import responses from poetry.factory import Factory if TYPE_CHECKING: from cleo.testers.application_tester import ApplicationTester from pytest_mock import MockerFixture from poetry.utils.env import VirtualEnv from tests.helpers import PoetryTestApplication from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter def test_publish_not_possible_in_non_package_mode( fixture_dir: FixtureDirGetter, command_tester_factory: CommandTesterFactory, ) -> None: source_dir = fixture_dir("non_package_mode") poetry = Factory().create_poetry(source_dir) tester = command_tester_factory("publish", poetry) assert tester.execute() == 1 assert ( tester.io.fetch_error() == "Publishing a package is not possible in non-package mode.\n" ) def test_publish_returns_non_zero_code_for_upload_errors( app: PoetryTestApplication, app_tester: ApplicationTester, http: responses.RequestsMock, ) -> None: http.post("https://upload.pypi.org/legacy/", status=400, body="Bad Request") exit_code = app_tester.execute("publish --username foo --password bar") assert exit_code == 1 expected_output = """ Publishing simple-project (1.2.3) to PyPI """ expected_error_output = """\ HTTP Error 400: Bad Request | b'Bad Request' """ assert expected_output in app_tester.io.fetch_output() assert expected_error_output in app_tester.io.fetch_error() @pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") def test_publish_returns_non_zero_code_for_connection_errors( app: PoetryTestApplication, app_tester: ApplicationTester, http: responses.RequestsMock, ) -> None: def request_callback(request: requests.PreparedRequest) -> NoReturn: raise requests.ConnectionError http.add_callback( responses.POST, "https://upload.pypi.org/legacy/", callback=request_callback ) exit_code = app_tester.execute("publish --username foo --password bar") assert exit_code == 1 assert "Error connecting to repository" in app_tester.io.fetch_error() def test_publish_with_cert( app_tester: ApplicationTester, mocker: MockerFixture ) -> None: publisher_publish = mocker.patch("poetry.publishing.Publisher.publish") app_tester.execute("publish --cert path/to/ca.pem") assert [ (None, None, None, Path("path/to/ca.pem"), None, False, False) ] == publisher_publish.call_args def test_publish_with_client_cert( app_tester: ApplicationTester, mocker: MockerFixture ) -> None: publisher_publish = mocker.patch("poetry.publishing.Publisher.publish") app_tester.execute("publish --client-cert path/to/client.pem") assert [ (None, None, None, None, Path("path/to/client.pem"), False, False) ] == publisher_publish.call_args @pytest.mark.parametrize( "options", [ "--dry-run", "--skip-existing", "--dry-run --skip-existing", ], ) def test_publish_dry_run_skip_existing( app_tester: ApplicationTester, http: responses.RequestsMock, options: str ) -> None: http.post("https://upload.pypi.org/legacy/", status=409, body="Conflict") exit_code = app_tester.execute(f"publish {options} --username foo --password bar") assert exit_code == 0 output = app_tester.io.fetch_output() error = app_tester.io.fetch_error() assert "Publishing simple-project (1.2.3) to PyPI" in output assert "- Uploading simple_project-1.2.3.tar.gz" in error assert "- Uploading simple_project-1.2.3-py2.py3-none-any.whl" in error def test_skip_existing_output( app_tester: ApplicationTester, http: responses.RequestsMock ) -> None: http.post("https://upload.pypi.org/legacy/", status=409, body="Conflict") exit_code = app_tester.execute( "publish --skip-existing --username foo --password bar" ) assert exit_code == 0 error = app_tester.io.fetch_error() assert "- Uploading simple_project-1.2.3.tar.gz File exists. Skipping" in error @pytest.mark.parametrize("dist_dir", [None, "dist", "other_dist/dist", "absolute"]) def test_publish_dist_dir_option( http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path, tmp_venv: VirtualEnv, command_tester_factory: CommandTesterFactory, dist_dir: str | None, ) -> None: source_dir = fixture_dir("with_multiple_dist_dir") target_dir = tmp_path / "project" shutil.copytree(str(source_dir), str(target_dir)) http.post("https://upload.pypi.org/legacy/", status=409, body="Conflict") poetry = Factory().create_poetry(target_dir) tester = command_tester_factory("publish", poetry, environment=tmp_venv) if dist_dir is None: exit_code = tester.execute("--dry-run") elif dist_dir == "absolute": exit_code = tester.execute(f"--dist-dir {target_dir / 'dist'} --dry-run") else: exit_code = tester.execute(f"--dist-dir {dist_dir} --dry-run") assert exit_code == 0 output = tester.io.fetch_output() error = tester.io.fetch_error() assert "Publishing simple-project (1.2.3) to PyPI" in output assert "- Uploading simple_project-1.2.3.tar.gz" in error assert "- Uploading simple_project-1.2.3-py2.py3-none-any.whl" in error @pytest.mark.parametrize("dist_dir", ["../dist", "tmp/dist", "absolute"]) def test_publish_dist_dir_and_build_options( http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path, tmp_venv: VirtualEnv, command_tester_factory: CommandTesterFactory, dist_dir: str | None, ) -> None: source_dir = fixture_dir("simple_project") target_dir = tmp_path / "project" shutil.copytree(str(source_dir), str(target_dir)) # Remove dist dir because as it will be built again shutil.rmtree(target_dir / "dist") http.post("https://upload.pypi.org/legacy/", status=409, body="Conflict") poetry = Factory().create_poetry(target_dir) tester = command_tester_factory("publish", poetry, environment=tmp_venv) if dist_dir == "absolute": exit_code = tester.execute( f"--dist-dir {target_dir / 'test/dist'} --dry-run --build" ) else: exit_code = tester.execute(f"--dist-dir {dist_dir} --dry-run --build") assert exit_code == 0 output = tester.io.fetch_output() error = tester.io.fetch_error() assert "Publishing simple-project (1.2.3) to PyPI" in output assert "- Uploading simple_project-1.2.3.tar.gz" in error assert "- Uploading simple_project-1.2.3-py2.py3-none-any.whl" in error ================================================ FILE: tests/console/commands/test_remove.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import cast import pytest import tomlkit from poetry.core.packages.dependency_group import DependencyGroup from poetry.core.packages.package import Package from poetry.factory import Factory from tests.helpers import TestLocker from tests.helpers import get_package if TYPE_CHECKING: from collections.abc import Callable from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from tomlkit import TOMLDocument from poetry.poetry import Poetry from poetry.repositories import Repository from tests.helpers import PoetryTestApplication from tests.helpers import TestRepository from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture def poetry_with_up_to_date_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Callable[[str], Poetry]: def get_poetry(fixture_name: str) -> Poetry: source = fixture_dir(fixture_name) poetry = project_factory( name="foobar", pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), poetry_lock_content=(source / "poetry.lock").read_text(encoding="utf-8"), ) assert isinstance(poetry.locker, TestLocker) poetry.locker.locked(True) return poetry return get_poetry @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("remove") def test_remove_from_project_and_poetry( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, installed: Repository, ) -> None: repo.add_package(Package("foo", "2.0.0")) repo.add_package(Package("bar", "1.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() project_dependencies: dict[str, Any] = tomlkit.parse( """\ [project] dependencies = [ "foo>=2.0", "bar>=1.0", ] """ ) poetry_dependencies: dict[str, Any] = tomlkit.parse( """\ [tool.poetry.dependencies] foo = "^2.0.0" bar = "^1.0.0" """ ) pyproject["project"]["dependencies"] = project_dependencies["project"][ "dependencies" ] pyproject["tool"]["poetry"]["dependencies"] = poetry_dependencies["tool"]["poetry"][ "dependencies" ] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0")) app.poetry.package.add_dependency(Factory.create_dependency("bar", "^1.0.0")) tester.execute("foo") pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) project_dependencies = pyproject["project"]["dependencies"] assert "foo>=2.0" not in project_dependencies assert "bar>=1.0" in project_dependencies poetry_dependencies = pyproject["tool"]["poetry"]["dependencies"] assert "foo" not in poetry_dependencies assert "bar" in poetry_dependencies expected_project_string = """\ dependencies = [ "bar>=1.0", ] """ expected_poetry_string = """\ [tool.poetry.dependencies] bar = "^1.0.0" """ pyproject = cast("TOMLDocument", pyproject) string_content = pyproject.as_string() if "\r\n" in string_content: # consistent line endings expected_project_string = expected_project_string.replace("\n", "\r\n") expected_poetry_string = expected_poetry_string.replace("\n", "\r\n") assert expected_project_string in string_content assert expected_poetry_string in string_content def test_remove_from_pep735_group_and_poetry_group( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, installed: Repository, ) -> None: repo.add_package(Package("foo", "2.0.0")) repo.add_package(Package("bar", "1.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() pep735_dependencies: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] dev = [ "foo>=2.0", "bar>=1.0", ] """ ) poetry_dependencies: dict[str, Any] = tomlkit.parse( """\ [tool.poetry.group.dev.dependencies] foo = "^2.0.0" bar = "^1.0.0" """ ) pyproject["dependency-groups"] = pep735_dependencies["dependency-groups"] pyproject["tool"]["poetry"]["group"] = poetry_dependencies["tool"]["poetry"][ "group" ] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package.add_dependency( Factory.create_dependency("foo", "^2.0.0", groups=["dev"]) ) app.poetry.package.add_dependency( Factory.create_dependency("bar", "^1.0.0", groups=["dev"]) ) tester.execute("foo") pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) pep735_dependencies = pyproject["dependency-groups"]["dev"] assert "foo>=2.0" not in pep735_dependencies assert "bar>=1.0" in pep735_dependencies poetry_dependencies = pyproject["tool"]["poetry"]["group"]["dev"]["dependencies"] assert "foo" not in poetry_dependencies assert "bar" in poetry_dependencies expected_pep735_string = """\ [dependency-groups] dev = [ "bar>=1.0", ] """ expected_poetry_string = """\ [tool.poetry.group.dev.dependencies] bar = "^1.0.0" """ pyproject = cast("TOMLDocument", pyproject) string_content = pyproject.as_string() if "\r\n" in string_content: # consistent line endings expected_pep735_string = expected_pep735_string.replace("\n", "\r\n") expected_poetry_string = expected_poetry_string.replace("\n", "\r\n") assert expected_pep735_string in string_content assert expected_poetry_string in string_content @pytest.mark.parametrize("pep_735", [True, False]) def test_remove_without_specific_group_removes_from_all_groups( pep_735: bool, tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, installed: Repository, ) -> None: """ Removing without specifying a group removes packages from all groups. """ installed.add_package(Package("foo", "2.0.0")) repo.add_package(Package("foo", "2.0.0")) repo.add_package(Package("baz", "1.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^2.0.0" if pep_735: groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] bar = [ "foo (>=2.0,<3.0)", "baz (>=1.0,<2.0)", ] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] else: groups_content = tomlkit.parse( """\ [tool.poetry.group.bar.dependencies] foo = "^2.0.0" baz = "^1.0.0" """ ) groups_content = cast("dict[str, Any]", groups_content) pyproject["tool"]["poetry"]["group"] = groups_content["tool"]["poetry"]["group"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0")) app.poetry.package.add_dependency( Factory.create_dependency("foo", "^2.0.0", groups=["bar"]) ) app.poetry.package.add_dependency( Factory.create_dependency("baz", "^1.0.0", groups=["bar"]) ) tester.execute("foo") pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) content = pyproject["tool"]["poetry"] assert "foo" not in content["dependencies"] if pep_735: assert not any("foo" in dep for dep in pyproject["dependency-groups"]["bar"]) assert any("baz" in dep for dep in pyproject["dependency-groups"]["bar"]) expected = """\ [dependency-groups] bar = [ "baz (>=1.0,<2.0)", ] """ else: assert "foo" not in content["group"]["bar"]["dependencies"] assert "baz" in content["group"]["bar"]["dependencies"] expected = """\ [tool.poetry.group.bar.dependencies] baz = "^1.0.0" """ pyproject = cast("TOMLDocument", pyproject) string_content = pyproject.as_string() if "\r\n" in string_content: # consistent line endings expected = expected.replace("\n", "\r\n") assert expected in string_content @pytest.mark.parametrize("pep_735", [True, False]) def test_remove_with_specific_group_removes_from_specific_groups( pep_735: bool, tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, installed: Repository, ) -> None: """ Removing with a specific group given removes packages only from this group. """ installed.add_package(Package("foo", "2.0.0")) repo.add_package(Package("foo", "2.0.0")) repo.add_package(Package("baz", "1.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^2.0.0" if pep_735: groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] bar = [ "foo (>=2.0,<3.0)", "baz (>=1.0,<2.0)", ] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] else: groups_content = tomlkit.parse( """\ [tool.poetry.group.bar.dependencies] foo = "^2.0.0" baz = "^1.0.0" """ ) groups_content = cast("dict[str, Any]", groups_content) pyproject["tool"]["poetry"]["group"] = groups_content["tool"]["poetry"]["group"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0")) app.poetry.package.add_dependency( Factory.create_dependency("foo", "^2.0.0", groups=["bar"]) ) app.poetry.package.add_dependency( Factory.create_dependency("baz", "^1.0.0", groups=["bar"]) ) tester.execute("foo --group bar") pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) content = pyproject["tool"]["poetry"] assert "foo" in content["dependencies"] if pep_735: assert not any("foo" in dep for dep in pyproject["dependency-groups"]["bar"]) assert any("baz" in dep for dep in pyproject["dependency-groups"]["bar"]) expected = """\ [dependency-groups] bar = [ "baz (>=1.0,<2.0)", ] """ else: assert "foo" not in content["group"]["bar"]["dependencies"] assert "baz" in content["group"]["bar"]["dependencies"] expected = """\ [tool.poetry.group.bar.dependencies] baz = "^1.0.0" """ pyproject = cast("TOMLDocument", pyproject) string_content = pyproject.as_string() if "\r\n" in string_content: # consistent line endings expected = expected.replace("\n", "\r\n") assert expected in string_content @pytest.mark.parametrize("pep_735", [True, False]) def test_remove_does_not_keep_empty_groups( pep_735: bool, tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, installed: Repository, ) -> None: """ Empty groups are automatically discarded after package removal. """ installed.add_package(Package("foo", "2.0.0")) repo.add_package(Package("foo", "2.0.0")) repo.add_package(Package("baz", "1.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^2.0.0" if pep_735: groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] bar = [ "foo (>=2.0,<3.0)", "baz (>=1.0,<2.0)", ] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] else: groups_content = tomlkit.parse( """\ [tool.poetry.group.bar.dependencies] foo = "^2.0.0" baz = "^1.0.0" """ ) groups_content = cast("dict[str, Any]", groups_content) pyproject["tool"]["poetry"]["group"] = groups_content["tool"]["poetry"]["group"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0")) app.poetry.package.add_dependency( Factory.create_dependency("foo", "^2.0.0", groups=["bar"]) ) app.poetry.package.add_dependency( Factory.create_dependency("baz", "^1.0.0", groups=["bar"]) ) tester.execute("foo baz --group bar") pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) content = pyproject["tool"]["poetry"] assert "foo" in content["dependencies"] if pep_735: assert "bar" not in pyproject.get("dependency-groups", {}) assert "dependency-groups" not in pyproject else: # The group 'bar' should be removed entirely from the configuration assert "group" not in content content = cast("TOMLDocument", content) assert "[tool.poetry.group.bar]" not in content.as_string() assert "[tool.poetry.group]" not in content.as_string() @pytest.mark.parametrize("pep_735", [True, False]) def test_remove_canonicalized_named_removes_dependency_correctly( pep_735: bool, tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, installed: Repository, ) -> None: """ Removing a dependency using a canonicalized named removes the dependency. """ installed.add_package(Package("foo-bar", "2.0.0")) repo.add_package(Package("foo-bar", "2.0.0")) repo.add_package(Package("baz", "1.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() pyproject["tool"]["poetry"]["dependencies"]["foo-bar"] = "^2.0.0" if pep_735: groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] bar = [ "foo-bar (>=2.0,<3.0)", "baz (>=1.0,<2.0)", ] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] else: groups_content = tomlkit.parse( """\ [tool.poetry.group.bar.dependencies] foo-bar = "^2.0.0" baz = "^1.0.0" """ ) groups_content = cast("dict[str, Any]", groups_content) pyproject["tool"]["poetry"].value._insert_after( "dependencies", "group", groups_content["tool"]["poetry"]["group"] ) pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package.add_dependency(Factory.create_dependency("foo-bar", "^2.0.0")) app.poetry.package.add_dependency( Factory.create_dependency("foo-bar", "^2.0.0", groups=["bar"]) ) app.poetry.package.add_dependency( Factory.create_dependency("baz", "^1.0.0", groups=["bar"]) ) tester.execute("Foo_Bar") pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) content = pyproject["tool"]["poetry"] assert "foo-bar" not in content["dependencies"] if pep_735: assert not any("foo" in dep for dep in pyproject["dependency-groups"]["bar"]) assert any("baz" in dep for dep in pyproject["dependency-groups"]["bar"]) expected = """\ [dependency-groups] bar = [ "baz (>=1.0,<2.0)", ] """ else: assert "foo-bar" not in content["group"]["bar"]["dependencies"] assert "baz" in content["group"]["bar"]["dependencies"] expected = """\ [tool.poetry.group.bar.dependencies] baz = "^1.0.0" """ pyproject = cast("TOMLDocument", pyproject) string_content = pyproject.as_string() if "\r\n" in string_content: # consistent line endings expected = expected.replace("\n", "\r\n") assert expected in string_content def test_remove_package_does_not_exist( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: repo.add_package(Package("foo", "2.0.0")) original_content = app.poetry.file.read().as_string() with pytest.raises(ValueError) as e: tester.execute("foo") assert str(e.value) == "The following packages were not found: foo" assert app.poetry.file.read().as_string() == original_content def test_remove_package_no_dependencies( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: repo.add_package(Package("foo", "2.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() assert "dependencies" not in pyproject["project"] del pyproject["tool"]["poetry"]["dependencies"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package._dependency_groups = {} with pytest.raises(ValueError) as e: tester.execute("foo") assert str(e.value) == "The following packages were not found: foo" def test_remove_command_should_not_write_changes_upon_installer_errors( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, ) -> None: repo.add_package(Package("foo", "2.0.0")) command_tester_factory("add").execute("foo") mocker.patch("poetry.installation.installer.Installer.run", return_value=1) original_content = app.poetry.file.read().as_string() tester.execute("foo") assert app.poetry.file.read().as_string() == original_content @pytest.mark.parametrize( "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] ) def test_remove_with_dry_run_keep_files_intact( fixture_name: str, poetry_with_up_to_date_lockfile: Callable[[str], Poetry], repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: poetry = poetry_with_up_to_date_lockfile(fixture_name) tester = command_tester_factory("remove", poetry=poetry) original_pyproject_content = poetry.file.read() original_lockfile_content = poetry._locker.lock_data repo.add_package(get_package("docker", "4.3.1")) tester.execute("docker --dry-run") assert poetry.file.read() == original_pyproject_content assert poetry._locker.lock_data == original_lockfile_content @pytest.mark.parametrize( "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] ) def test_remove_performs_uninstall_op( fixture_name: str, poetry_with_up_to_date_lockfile: Callable[[str], Poetry], command_tester_factory: CommandTesterFactory, installed: Repository, ) -> None: installed.add_package(get_package("docker", "4.3.1")) poetry = poetry_with_up_to_date_lockfile(fixture_name) tester = command_tester_factory("remove", poetry=poetry) tester.execute("docker") expected = """\ Updating dependencies Resolving dependencies... Package operations: 0 installs, 0 updates, 1 removal - Removing docker (4.3.1) Writing lock file """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize( "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] ) def test_remove_with_lock_does_not_perform_uninstall_op( fixture_name: str, poetry_with_up_to_date_lockfile: Callable[[str], Poetry], command_tester_factory: CommandTesterFactory, installed: Repository, ) -> None: installed.add_package(get_package("docker", "4.3.1")) poetry = poetry_with_up_to_date_lockfile(fixture_name) tester = command_tester_factory("remove", poetry=poetry) tester.execute("docker --lock") expected = """\ Updating dependencies Resolving dependencies... Writing lock file """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize("pep_735", [True, False]) def test_remove_from_nested_pep735_group_and_poetry_group( pep_735: bool, tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, installed: Repository, ) -> None: """ Test removing packages from nested dependency groups with `include-group`(pep735) or `include-groups`(poetry). """ installed.add_package(Package("foo", "2.0.0")) installed.add_package(Package("baz", "1.0.0")) repo.add_package(Package("foo", "2.0.0")) repo.add_package(Package("baz", "1.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^2.0.0" if pep_735: groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] bar = [ "foo (>=2.0,<3.0)", ] foobar = [ { include-group = "bar" }, "baz (>=1.0,<2.0)", ] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] else: groups_content = tomlkit.parse( """\ [tool.poetry.group.bar.dependencies] foo = "^2.0.0" [tool.poetry.group.foobar] include-groups = [ "bar", ] [tool.poetry.group.foobar.dependencies] baz = "^1.0.0" """ ) groups_content = cast("dict[str, Any]", groups_content) pyproject["tool"]["poetry"]["group"] = groups_content["tool"]["poetry"]["group"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0")) app.poetry.package.add_dependency( Factory.create_dependency("foo", "^2.0.0", groups=["bar"]) ) app.poetry.package.add_dependency( Factory.create_dependency("baz", "^1.0.0", groups=["foobar"]) ) tester.execute("baz") pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) content = pyproject["tool"]["poetry"] if pep_735: assert "baz" not in pyproject["dependency-groups"]["foobar"] assert any( isinstance(item, dict) and item.get("include-group") == "bar" for item in pyproject["dependency-groups"]["foobar"] ) expected = """\ [dependency-groups] bar = [ "foo (>=2.0,<3.0)", ] foobar = [ { include-group = "bar" }, ] """ else: # This also checks that the include-groups section is not removed even if its explicit dependencies are empty assert "dependencies" not in content["group"]["foobar"] expected = """\ [tool.poetry.group.bar.dependencies] foo = "^2.0.0" [tool.poetry.group.foobar] include-groups = [ "bar", ] """ pyproject = cast("TOMLDocument", pyproject) string_content = pyproject.as_string() if "\r\n" in string_content: # consistent line endings expected = expected.replace("\n", "\r\n") assert expected in string_content @pytest.mark.parametrize("pep_735", [True, False]) @pytest.mark.parametrize("args", ["", " --group bar"]) def test_remove_group_cleans_up_include_group_references( pep_735: bool, args: str, tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, installed: Repository, ) -> None: """ When a group is removed, any `include-group` references to it in other groups should also be cleaned up. """ installed.add_package(Package("foo", "2.0.0")) installed.add_package(Package("baz", "1.0.0")) repo.add_package(Package("foo", "2.0.0")) repo.add_package(Package("baz", "1.0.0")) pyproject: dict[str, Any] = app.poetry.file.read() if pep_735: groups_content: dict[str, Any] = tomlkit.parse( """\ [dependency-groups] bar = [ "foo (>=2.0,<3.0)", ] foobar = [ { include-group = "bar" }, ] foobar2 = [ "baz (>=1.0)", { include-group = "bar" }, "baz (<=3.0)", ] foobar3 = [ { include-group = "bar" }, { include-group = "foobar2" }, ] """ ) pyproject["dependency-groups"] = groups_content["dependency-groups"] else: groups_content = tomlkit.parse( """\ [tool.poetry.group.bar.dependencies] foo = "^2.0.0" [tool.poetry.group.foobar] include-groups = [ "bar", ] [tool.poetry.group.foobar2] include-groups = [ "bar", ] [tool.poetry.group.foobar2.dependencies] baz = "(>=1.0,<=3.0)" [tool.poetry.group.foobar3] include-groups = [ "bar", "foobar2", ] """ ) groups_content = cast("dict[str, Any]", groups_content) pyproject["tool"]["poetry"]["group"] = groups_content["tool"]["poetry"]["group"] pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) app.poetry.package.add_dependency( Factory.create_dependency("foo", "^2.0.0", groups=["bar"]) ) app.poetry.package.add_dependency( Factory.create_dependency("baz", "^1.0.0", groups=["foobar2"]) ) foobar = DependencyGroup("foobar") foobar.include_dependency_group(app.poetry.package.dependency_group("bar")) app.poetry.package.add_dependency_group(foobar) foobar2 = DependencyGroup("foobar2") foobar2.include_dependency_group(app.poetry.package.dependency_group("bar")) app.poetry.package.add_dependency_group(foobar2) foobar3 = DependencyGroup("foobar3") foobar3.include_dependency_group(app.poetry.package.dependency_group("bar")) foobar3.include_dependency_group(app.poetry.package.dependency_group("foobar2")) app.poetry.package.add_dependency_group(foobar3) # Remove all packages from the "bar" group, which should delete the group # and also clean up references to it in "foobar" tester.execute(f"foo{args}") pyproject = app.poetry.file.read() pyproject = cast("dict[str, Any]", pyproject) content = pyproject["tool"]["poetry"] if pep_735: # "bar" group should be removed assert "bar" not in pyproject.get("dependency-groups", {}) # "foobar" group should also be removed since it only had include-group assert "foobar" not in pyproject.get("dependency-groups", {}) # "foobar2" should have its include-group cleaned up assert "foobar2" in pyproject.get("dependency-groups", {}) assert pyproject["dependency-groups"]["foobar2"] == [ "baz (>=1.0)", "baz (<=3.0)", ] # "foobar3" should have its include-groups cleaned up assert "foobar3" in pyproject.get("dependency-groups", {}) assert pyproject["dependency-groups"]["foobar3"] == [ {"include-group": "foobar2"} ] else: # "bar" group should be removed assert "bar" not in content.get("group", {}) # "foobar" group should also be removed since it only had include-group assert "foobar" not in content.get("group", {}) # "foobar2" should have its include-groups cleaned up assert "foobar2" in content.get("group", {}) assert "include-groups" not in content["group"]["foobar2"] assert "dependencies" in content["group"]["foobar2"] assert content["group"]["foobar2"]["dependencies"] == {"baz": "(>=1.0,<=3.0)"} # "foobar3" should have its include-groups cleaned up assert "foobar3" in content.get("group", {}) assert "include-groups" in content["group"]["foobar3"] assert content["group"]["foobar3"]["include-groups"] == ["foobar2"] ================================================ FILE: tests/console/commands/test_run.py ================================================ from __future__ import annotations import subprocess from typing import TYPE_CHECKING import pytest from poetry.utils._compat import WINDOWS if TYPE_CHECKING: from cleo.testers.application_tester import ApplicationTester from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture from poetry.poetry import Poetry from poetry.utils.env import MockEnv from poetry.utils.env import VirtualEnv from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("run") @pytest.fixture(autouse=True) def patches(mocker: MockerFixture, env: MockEnv) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=env) @pytest.fixture def poetry_with_scripts( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: source = fixture_dir("scripts") return project_factory( name="scripts", pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), source=source, ) def test_run_passes_all_args(app_tester: ApplicationTester, env: MockEnv) -> None: app_tester.execute("run python -V") assert env.executed == [["python", "-V"]] def test_run_is_not_eager(app_tester: ApplicationTester, env: MockEnv) -> None: app_tester.execute("--no-ansi -C run -install", decorated=True) assert ( app_tester.io.fetch_error().strip() == "Specified path 'run' is not a valid directory." ) assert env.executed == [] def test_run_passes_args_after_run_before_command( app_tester: ApplicationTester, env: MockEnv ) -> None: app_tester.execute("run -P. python -V", decorated=True) assert env.executed == [["python", "-V"]] @pytest.mark.parametrize( "args", [ "-vP run run", "run -vP run", "-vPrun run", "run -vPrun ", "-v --project=run run", "-v run --project=run", "-v --directory=run run", "run -v --directory=run", ], ) def test_run_passes_args_after_run_before_command_name_conflict( args: str, app_tester: ApplicationTester, env: MockEnv, project_factory: ProjectFactory, ) -> None: poetry = project_factory("run") path = poetry.file.path.parent path.rename(path.parent / "run") app_tester.execute(f"{args} python -V", decorated=True) assert ( app_tester.io.remove_format(app_tester.io.fetch_error()) == f"Using virtualenv: {env.path}\n" ) assert env.executed == [["python", "-V"]] def test_run_keeps_options_passed_before_command_args_combined_short_opts( app_tester: ApplicationTester, env: MockEnv ) -> None: app_tester.execute("run -VP. --no-ansi python", decorated=True) assert not app_tester.io.is_decorated() assert app_tester.io.fetch_output() == app_tester.io.remove_format( app_tester.application.long_version + "\n" ) assert env.executed == [] def test_run_keeps_options_passed_before_command_args( app_tester: ApplicationTester, env: MockEnv ) -> None: app_tester.execute("run -V --no-ansi python", decorated=True) assert not app_tester.io.is_decorated() assert app_tester.io.fetch_output() == app_tester.io.remove_format( app_tester.application.long_version + "\n" ) assert env.executed == [] def test_run_keeps_options_passed_before_command( app_tester: ApplicationTester, env: MockEnv ) -> None: app_tester.execute("-V --no-ansi run python", decorated=True) assert not app_tester.io.is_decorated() assert app_tester.io.fetch_output() == app_tester.io.remove_format( app_tester.application.long_version + "\n" ) assert env.executed == [] def test_run_has_helpful_error_when_command_not_found( app_tester: ApplicationTester, env: MockEnv, capfd: pytest.CaptureFixture[str] ) -> None: nonexistent_command = "nonexistent-command" env._execute = True app_tester.execute(f"run {nonexistent_command}") assert env.executed == [[nonexistent_command]] assert app_tester.status_code == 1 if WINDOWS: # On Windows we use a shell to run commands which provides its own error # message when a command is not found that is not captured by the # ApplicationTester but is captured by pytest, and we can access it via capfd. # The exact error message depends on the system language. Thus, we check only # for the name of the command. assert nonexistent_command in capfd.readouterr().err else: assert ( app_tester.io.fetch_error() == f"Command not found: {nonexistent_command}\n" ) @pytest.mark.skipif( not WINDOWS, reason=( "Poetry only installs CMD script files for console scripts of editable" " dependencies on Windows" ), ) def test_run_console_scripts_of_editable_dependencies_on_windows( tmp_venv: VirtualEnv, command_tester_factory: CommandTesterFactory, ) -> None: """ On Windows, Poetry installs console scripts of editable dependencies by creating in the environment's `Scripts/` directory both: A) a Python file named after the console script (no `.py` extension) which imports and calls the console script using Python code B) a CMD script file also named after the console script (with `.cmd` extension) which calls `python.exe` to execute (A) This configuration enables calling the console script by name from `cmd.exe` because the `.cmd` file extension appears by default in the PATHEXT environment variable that `cmd.exe` uses to determine which file should be executed if a filename without an extension is executed as a command. This test validates that you can also run such a CMD script file via `poetry run` just by providing the script's name without the `.cmd` extension. """ tester = command_tester_factory("run", environment=tmp_venv) cmd_script_file = tmp_venv._bin_dir / "quix.cmd" # `/b` ensures we only exit the script instead of any cmd.exe proc that called it cmd_script_file.write_text("exit /b 123", encoding="locale") # We prove that the CMD script executed successfully by verifying the exit code # matches what we wrote in the script assert tester.execute("quix") == 123 def test_run_script_exit_code( poetry_with_scripts: Poetry, command_tester_factory: CommandTesterFactory, tmp_venv: VirtualEnv, mocker: MockerFixture, ) -> None: mocker.patch( "os.execvpe", lambda file, args, env: subprocess.call([file, *args[1:]], env=env), ) install_tester = command_tester_factory( "install", poetry=poetry_with_scripts, environment=tmp_venv, ) assert install_tester.execute() == 0 tester = command_tester_factory( "run", poetry=poetry_with_scripts, environment=tmp_venv ) assert tester.execute("exit-code") == 42 assert tester.execute("return-code") == 42 @pytest.mark.parametrize( "installed_script", [False, True], ids=["not installed", "installed"] ) def test_run_script_sys_argv0( installed_script: bool, poetry_with_scripts: Poetry, command_tester_factory: CommandTesterFactory, tmp_venv: VirtualEnv, mocker: MockerFixture, ) -> None: """ If RunCommand calls an installed script defined in pyproject.toml, sys.argv[0] must be set to the full path of the script. """ mocker.patch("poetry.utils.env.EnvManager.get", return_value=tmp_venv) mocker.patch( "os.execvpe", lambda file, args, env: subprocess.call([file, *args[1:]], env=env), ) install_tester = command_tester_factory( "install", poetry=poetry_with_scripts, environment=tmp_venv, ) assert install_tester.execute() == 0 if not installed_script: for path in tmp_venv.script_dirs[0].glob("check-argv0*"): path.unlink() tester = command_tester_factory( "run", poetry=poetry_with_scripts, environment=tmp_venv ) argv1 = "absolute" if installed_script else "relative" assert tester.execute(f"check-argv0 {argv1}") == 0 if installed_script: expected_message = "" else: expected_message = """\ Warning: 'check-argv0' is an entry point defined in pyproject.toml, but it's not \ installed as a script. You may get improper `sys.argv[0]`. The support to run uninstalled scripts will be removed in a future release. Run `poetry install` to resolve and get rid of this message. """ assert tester.io.fetch_error() == expected_message ================================================ FILE: tests/console/commands/test_search.py ================================================ from __future__ import annotations import re from typing import TYPE_CHECKING import pytest from poetry.repositories.pypi_repository import PyPiRepository if TYPE_CHECKING: import responses from cleo.testers.command_tester import CommandTester from poetry.poetry import Poetry from poetry.repositories.legacy_repository import LegacyRepository from tests.types import CommandTesterFactory SQLALCHEMY_SEARCH_OUTPUT_PYPI = """\ Package Version Source Description broadway-sqlalchemy 0.0.1 PyPI A broadway extension wrapping Flask-SQLAlchemy cherrypy-sqlalchemy 0.5.3 PyPI Use SQLAlchemy with CherryPy graphene-sqlalchemy 2.2.2 PyPI Graphene SQLAlchemy integration jsonql-sqlalchemy 1.0.1 PyPI Simple JSON-Based CRUD Query Language for SQLAlchemy paginate-sqlalchemy 0.3.0 PyPI Extension to paginate.Page that supports SQLAlchemy queries sqlalchemy 1.3.10 PyPI Database Abstraction Library sqlalchemy-audit 0.1.0 PyPI sqlalchemy-audit provides an easy way to set up revision tracking for your data. sqlalchemy-dao 1.3.1 PyPI Simple wrapper for sqlalchemy. sqlalchemy-diff 0.1.3 PyPI Compare two database schemas using sqlalchemy. sqlalchemy-equivalence 0.1.1 PyPI Provides natural equivalence support for SQLAlchemy declarative models. sqlalchemy-filters 0.10.0 PyPI A library to filter SQLAlchemy queries. sqlalchemy-nav 0.0.2 PyPI SQLAlchemy-Nav provides SQLAlchemy Mixins for creating navigation bars compatible with Bootstrap sqlalchemy-plus 0.2.0 PyPI Create Views and Materialized Views with SqlAlchemy sqlalchemy-repr 0.0.1 PyPI Automatically generates pretty repr of a SQLAlchemy model. sqlalchemy-schemadisplay 1.3 PyPI Turn SQLAlchemy DB Model into a graph sqlalchemy-sqlany 1.0.3 PyPI SAP Sybase SQL Anywhere dialect for SQLAlchemy sqlalchemy-traversal 0.5.2 PyPI UNKNOWN sqlalchemy-utcdatetime 1.0.4 PyPI Convert to/from timezone aware datetimes when storing in a DBMS sqlalchemy-wrap 2.1.7 PyPI Python wrapper for the CircleCI API transmogrify-sqlalchemy 1.0.2 PyPI Feed data from SQLAlchemy into a transmogrifier pipeline """ @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("search") def clean_output(text: str) -> str: return re.sub(r"\s+\n", "\n", text) def test_search( tester: CommandTester, http: responses.RequestsMock, poetry: Poetry ) -> None: # we expect PyPI in the default behaviour poetry.pool.add_repository(PyPiRepository()) tester.execute("sqlalchemy") output = clean_output(tester.io.fetch_output()) assert output == SQLALCHEMY_SEARCH_OUTPUT_PYPI def test_search_empty_results( tester: CommandTester, http: responses.RequestsMock, poetry: Poetry, legacy_repository: LegacyRepository, ) -> None: poetry.pool.add_repository(legacy_repository) tester.execute("does-not-exist") output = tester.io.fetch_output() assert output.strip() == "No matching packages were found." def test_search_with_legacy_repository( tester: CommandTester, http: responses.RequestsMock, poetry: Poetry, legacy_repository: LegacyRepository, ) -> None: poetry.pool.add_repository(PyPiRepository()) poetry.pool.add_repository(legacy_repository) tester.execute("sqlalchemy") line_before = " sqlalchemy-filters 0.10.0 PyPI A library to filter SQLAlchemy queries." additional_line = " sqlalchemy-legacy 4.3.4 legacy" expected = SQLALCHEMY_SEARCH_OUTPUT_PYPI.replace( line_before, f"{line_before}\n{additional_line}" ) output = clean_output(tester.io.fetch_output()) assert output == expected def test_search_only_legacy_repository( tester: CommandTester, http: responses.RequestsMock, poetry: Poetry, legacy_repository: LegacyRepository, ) -> None: poetry.pool.add_repository(legacy_repository) tester.execute("ipython") expected = """\ Package Version Source Description ipython 4.1.0rc1 legacy ipython 5.7.0 legacy ipython 7.5.0 legacy """ output = clean_output(tester.io.fetch_output()) assert output == expected def test_search_multiple_queries( tester: CommandTester, http: responses.RequestsMock, poetry: Poetry, legacy_repository: LegacyRepository, ) -> None: poetry.pool.add_repository(legacy_repository) tester.execute("ipython isort") expected = """\ Package Version Source Description ipython 4.1.0rc1 legacy ipython 5.7.0 legacy ipython 7.5.0 legacy isort 4.3.4 legacy isort-metadata 4.3.4 legacy """ output = clean_output(tester.io.fetch_output()) # we use a set here to avoid ordering issues assert set(output.split("\n")) == set(expected.split("\n")) ================================================ FILE: tests/console/commands/test_show.py ================================================ from __future__ import annotations import json from collections.abc import Callable from typing import TYPE_CHECKING from typing import Any from typing import TypeVar from typing import cast import pytest from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.dependency_group import DependencyGroup from poetry.factory import Factory from poetry.utils._compat import tomllib from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import TestLocker from tests.helpers import get_package if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from poetry.poetry import Poetry from poetry.repositories import Repository from tests.helpers import TestRepository from tests.types import CommandTesterFactory @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("show") F = TypeVar("F", bound=Callable[..., Any]) def output_format_parametrize(func: F) -> F: formats = ["", "--format json"] return cast("F", pytest.mark.parametrize("output_format", formats)(func)) @output_format_parametrize def test_show_basic_with_installed_packages( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) poetry.package.add_dependency( Factory.create_dependency("pytest", "^3.7.3", groups=["dev"]) ) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" pytest_373 = get_package("pytest", "3.7.3") pytest_373.description = "Pytest package" installed.add_package(cachy_010) installed.add_package(pendulum_200) installed.add_package(pytest_373) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pytest", "version": "3.7.3", "description": "Pytest package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": [], "pytest": []}, }, } ) tester.execute(output_format) expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, { "name": "pytest", "version": "3.7.3", "description": "Pytest package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 Cachy package pendulum 2.0.0 Pendulum package pytest 3.7.3 Pytest package """ assert tester.io.fetch_output() == expected def _configure_project_with_groups(poetry: Poetry, installed: Repository) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency_group(DependencyGroup(name="time", optional=True)) poetry.package.add_dependency( Factory.create_dependency("pendulum", "^2.0.0", groups=["time"]) ) poetry.package.add_dependency( Factory.create_dependency("pytest", "^3.7.3", groups=["test"]) ) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" pytest_373 = get_package("pytest", "3.7.3") pytest_373.description = "Pytest package" installed.add_package(cachy_010) installed.add_package(pendulum_200) installed.add_package(pytest_373) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pytest", "version": "3.7.3", "description": "Pytest package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": [], "pytest": []}, }, } ) @pytest.mark.parametrize( ("options", "expected"), [ ( "", """\ cachy 0.1.0 Cachy package pytest 3.7.3 Pytest package """, ), ( "--format json", [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "pytest", "version": "3.7.3", "description": "Pytest package", "installed_status": "installed", }, ], ), ( "--with time", """\ cachy 0.1.0 Cachy package pendulum 2.0.0 Pendulum package pytest 3.7.3 Pytest package """, ), ( "--with time --format json", [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, { "name": "pytest", "version": "3.7.3", "description": "Pytest package", "installed_status": "installed", }, ], ), ( "--without test", """\ cachy 0.1.0 Cachy package """, ), ( "--without test --format json", [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "installed", }, ], ), ( f"--without {MAIN_GROUP}", """\ pytest 3.7.3 Pytest package """, ), ( f"--without {MAIN_GROUP} --format json", [ { "name": "pytest", "version": "3.7.3", "description": "Pytest package", "installed_status": "installed", }, ], ), ( f"--only {MAIN_GROUP}", """\ cachy 0.1.0 Cachy package """, ), ( f"--only {MAIN_GROUP} --format json", [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "installed", }, ], ), ( "--with time --without test", """\ cachy 0.1.0 Cachy package pendulum 2.0.0 Pendulum package """, ), ( "--with time --without test --format json", [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, ], ), ( f"--with time --without {MAIN_GROUP},test", """\ pendulum 2.0.0 Pendulum package """, ), ( f"--with time --without {MAIN_GROUP},test --format json", [ { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, ], ), ( "--only time", """\ pendulum 2.0.0 Pendulum package """, ), ( "--only time --format json", [ { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, ], ), ( "--only time --with test", """\ pendulum 2.0.0 Pendulum package """, ), ( "--only time --with test --format json", [ { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, ], ), ( "--with time", """\ cachy 0.1.0 Cachy package pendulum 2.0.0 Pendulum package pytest 3.7.3 Pytest package """, ), ( "--with time --format json", [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, { "name": "pytest", "version": "3.7.3", "description": "Pytest package", "installed_status": "installed", }, ], ), ], ) def test_show_basic_with_group_options( options: str, expected: str | list[dict[str, str]], tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: _configure_project_with_groups(poetry, installed) tester.execute(options) if "json" in options: assert json.loads(tester.io.fetch_output()) == expected else: assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_basic_with_installed_packages_single( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" installed.add_package(cachy_010) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": []}, }, } ) tester.execute(f"cachy {output_format}") expected: dict[str, str] | list[str] = {} if "json" in output_format: expected = {"name": "cachy", "version": "0.1.0", "description": "Cachy package"} assert json.loads(tester.io.fetch_output()) == expected else: expected = [ "name : cachy", "version : 0.1.0", "description : Cachy package", ] assert [ line.strip() for line in tester.io.fetch_output().splitlines() ] == expected @output_format_parametrize def test_show_basic_with_installed_packages_single_canonicalized( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("foo-bar", "^0.1.0")) foo_bar = get_package("foo-bar", "0.1.0") foo_bar.description = "Foobar package" installed.add_package(foo_bar) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "foo-bar", "version": "0.1.0", "description": "Foobar package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"foo-bar": []}, }, } ) tester.execute(f"Foo_Bar {output_format}") expected: dict[str, str] | list[str] = {} if "json" in output_format: expected = { "name": "foo-bar", "version": "0.1.0", "description": "Foobar package", } assert json.loads(tester.io.fetch_output()) == expected else: expected = [ "name : foo-bar", "version : 0.1.0", "description : Foobar package", ] assert [ line.strip() for line in tester.io.fetch_output().splitlines() ] == expected @output_format_parametrize def test_show_basic_with_not_installed_packages_non_decorated( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" installed.add_package(cachy_010) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(output_format) expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "not-installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 Cachy package pendulum (!) 2.0.0 Pendulum package """ assert tester.io.fetch_output() == expected def test_show_basic_with_not_installed_packages_decorated( tester: CommandTester, poetry: Poetry, installed: Repository ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" installed.add_package(cachy_010) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(decorated=True) expected = """\ \033[36mcachy \033[39m \033[39;1m0.1.0\033[39;22m Cachy package \033[31mpendulum\033[39m \033[39;1m2.0.0\033[39;22m Pendulum package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_latest_non_decorated( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" pendulum_201 = get_package("pendulum", "2.0.1") pendulum_201.description = "Pendulum package" installed.add_package(cachy_010) installed.add_package(pendulum_200) repo.add_package(cachy_010) repo.add_package(cachy_020) repo.add_package(pendulum_200) repo.add_package(pendulum_201) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(f"--latest {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "latest_version": "0.2.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "pendulum", "version": "2.0.0", "latest_version": "2.0.1", "description": "Pendulum package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 0.2.0 Cachy package pendulum 2.0.0 2.0.1 Pendulum package """ assert tester.io.fetch_output() == expected def test_show_latest_decorated( tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" pendulum_201 = get_package("pendulum", "2.0.1") pendulum_201.description = "Pendulum package" installed.add_package(cachy_010) installed.add_package(pendulum_200) repo.add_package(cachy_010) repo.add_package(cachy_020) repo.add_package(pendulum_200) repo.add_package(pendulum_201) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute("--latest", decorated=True) expected = """\ \033[36mcachy \033[39m \033[39;1m0.1.0\033[39;22m\ \033[33m0.2.0\033[39m Cachy package \033[36mpendulum\033[39m \033[39;1m2.0.0\033[39;22m\ \033[31m2.0.1\033[39m Pendulum package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_outdated( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" installed.add_package(cachy_010) installed.add_package(pendulum_200) repo.add_package(cachy_010) repo.add_package(cachy_020) repo.add_package(pendulum_200) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "latest_version": "0.2.0", "description": "Cachy package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 0.2.0 Cachy package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_outdated_with_only_up_to_date_packages( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" installed.add_package(cachy_020) repo.add_package(cachy_020) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.2.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": []}, }, } ) tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [] assert json.loads(tester.io.fetch_output()) == expected else: expected = "" assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_outdated_has_prerelease_but_not_allowed( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" cachy_030dev = get_package("cachy", "0.3.0.dev123") cachy_030dev.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" installed.add_package(cachy_010) installed.add_package(pendulum_200) # sorting isn't used, so this has to be the first element to # replicate the issue in PR #1548 repo.add_package(cachy_030dev) repo.add_package(cachy_010) repo.add_package(cachy_020) repo.add_package(pendulum_200) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "latest_version": "0.2.0", "description": "Cachy package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 0.2.0 Cachy package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_outdated_has_prerelease_and_allowed( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency( Factory.create_dependency( "cachy", {"version": ">=0.0.1", "allow-prereleases": True} ) ) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010dev = get_package("cachy", "0.1.0.dev1") cachy_010dev.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" cachy_030dev = get_package("cachy", "0.3.0.dev123") cachy_030dev.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" installed.add_package(cachy_010dev) installed.add_package(pendulum_200) # sorting isn't used, so this has to be the first element to # replicate the issue in PR #1548 repo.add_package(cachy_030dev) repo.add_package(cachy_010dev) repo.add_package(cachy_020) repo.add_package(pendulum_200) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0.dev1", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0.dev1", "latest_version": "0.3.0.dev123", "description": "Cachy package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0.dev1 0.3.0.dev123 Cachy package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_outdated_formatting( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" pendulum_201 = get_package("pendulum", "2.0.1") pendulum_201.description = "Pendulum package" installed.add_package(cachy_010) installed.add_package(pendulum_200) repo.add_package(cachy_010) repo.add_package(cachy_020) repo.add_package(pendulum_200) repo.add_package(pendulum_201) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "latest_version": "0.2.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "pendulum", "version": "2.0.0", "latest_version": "2.0.1", "description": "Pendulum package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 0.2.0 Cachy package pendulum 2.0.0 2.0.1 Pendulum package """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize( ("project_directory", "required_fixtures"), [ ( "project_with_local_dependencies", ["distributions/demo-0.1.0-py2.py3-none-any.whl", "project_with_setup"], ), ], ) @output_format_parametrize def test_show_outdated_local_dependencies( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" cachy_030 = get_package("cachy", "0.3.0") cachy_030.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" demo_010 = get_package("demo", "0.1.0") demo_010.description = "" my_package_011 = get_package("project-with-setup", "0.1.1") my_package_011.description = "Demo project." installed.add_package(cachy_020) installed.add_package(pendulum_200) installed.add_package(demo_010) installed.add_package(my_package_011) repo.add_package(cachy_020) repo.add_package(cachy_030) repo.add_package(pendulum_200) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.2.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "demo", "version": "0.1.0", "description": "Demo package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "source": { "type": "file", "reference": "", "url": "../distributions/demo-0.1.0-py2.py3-none-any.whl", }, }, { "name": "project-with-setup", "version": "0.1.1", "description": "Demo project.", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": { "pendulum": ">=1.4.4", "cachy": {"version": ">=0.2.0", "extras": ["msgpack"]}, }, "source": { "type": "directory", "reference": "", "url": "../project_with_setup", }, }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": { "cachy": [], "pendulum": [], "demo": [], "project-with-setup": [], }, }, } ) tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.2.0", "latest_version": "0.3.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "project-with-setup", "version": "0.1.1 ../project_with_setup", "latest_version": "0.1.2 ../project_with_setup", "description": "Demo project.", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.2.0 0.3.0 project-with-setup 0.1.1 ../project_with_setup 0.1.2 ../project_with_setup """ assert ( "\n".join(line.rstrip() for line in tester.io.fetch_output().splitlines()) == expected.rstrip() ) @pytest.mark.parametrize("project_directory", ["project_with_git_dev_dependency"]) @output_format_parametrize def test_show_outdated_git_dev_dependency( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" demo_011 = get_package("demo", "0.1.1") demo_011.description = "Demo package" pytest = get_package("pytest", "3.4.3") pytest.description = "Pytest" installed.add_package(cachy_010) installed.add_package(pendulum_200) installed.add_package(demo_011) installed.add_package(pytest) repo.add_package(cachy_010) repo.add_package(cachy_020) repo.add_package(pendulum_200) repo.add_package(pytest) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "demo", "version": "0.1.1", "description": "Demo package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "source": { "type": "git", "reference": MOCK_DEFAULT_GIT_REVISION, "resolved_reference": MOCK_DEFAULT_GIT_REVISION, "url": "https://github.com/demo/demo.git", }, }, { "name": "pytest", "version": "3.4.3", "description": "Pytest", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": [], "demo": [], "pytest": []}, }, } ) tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "latest_version": "0.2.0", "description": "Cachy package", "installed_status": "installed", }, { "name": "demo", "version": "0.1.1 9cf87a2", "latest_version": "0.1.2 9cf87a2", "description": "Demo package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 0.2.0 Cachy package demo 0.1.1 9cf87a2 0.1.2 9cf87a2 Demo package """ assert tester.io.fetch_output() == expected @pytest.mark.parametrize("project_directory", ["project_with_git_dev_dependency"]) @output_format_parametrize def test_show_outdated_no_dev_git_dev_dependency( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" demo_011 = get_package("demo", "0.1.1") demo_011.description = "Demo package" pytest = get_package("pytest", "3.4.3") pytest.description = "Pytest" installed.add_package(cachy_010) installed.add_package(pendulum_200) installed.add_package(demo_011) installed.add_package(pytest) repo.add_package(cachy_010) repo.add_package(cachy_020) repo.add_package(pendulum_200) repo.add_package(pytest) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "demo", "version": "0.1.1", "description": "Demo package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "source": { "type": "git", "reference": MOCK_DEFAULT_GIT_REVISION, "url": "https://github.com/demo/pyproject-demo.git", }, }, { "name": "pytest", "version": "3.4.3", "description": "Pytest", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": [], "demo": [], "pytest": []}, }, } ) tester.execute(f"--outdated --without dev {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "latest_version": "0.2.0", "description": "Cachy package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 0.2.0 Cachy package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_hides_incompatible_package( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency( Factory.create_dependency("cachy", {"version": "^0.1.0", "python": "< 2.0"}) ) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" installed.add_package(pendulum_200) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(output_format) expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ pendulum 2.0.0 Pendulum package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_all_shows_incompatible_package( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" installed.add_package(pendulum_200) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "requirements": {"python": "1.0"}, }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": []}, }, } ) tester.execute(f"--all {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "not-installed", }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 Cachy package pendulum 2.0.0 Pendulum package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_hides_incompatible_package_with_duplicate( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency( Factory.create_dependency("cachy", {"version": "0.1.0", "platform": "linux"}) ) poetry.package.add_dependency( Factory.create_dependency("cachy", {"version": "0.1.1", "platform": "darwin"}) ) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "files": [], }, { "name": "cachy", "version": "0.1.1", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "files": [], }, ], "metadata": {"content-hash": "123456789"}, } ) tester.execute(output_format) expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.1", "description": "Cachy package", "installed_status": "not-installed", } ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy (!) 0.1.1 Cachy package """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_all_shows_all_duplicates( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: poetry.package.add_dependency( Factory.create_dependency("cachy", {"version": "0.1.0", "platform": "linux"}) ) poetry.package.add_dependency( Factory.create_dependency("cachy", {"version": "0.1.1", "platform": "darwin"}) ) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "files": [], }, { "name": "cachy", "version": "0.1.1", "description": "Cachy package", "optional": False, "platform": "*", "python-versions": "*", "files": [], }, ], "metadata": {"content-hash": "123456789"}, } ) tester.execute(f"--all {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.1.0", "description": "Cachy package", "installed_status": "not-installed", }, { "name": "cachy", "version": "0.1.1", "description": "Cachy package", "installed_status": "not-installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ cachy 0.1.0 Cachy package cachy (!) 0.1.1 Cachy package """ assert tester.io.fetch_output() == expected def test_show_tree( tester: CommandTester, poetry: Poetry, installed: Repository ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) cachy2 = get_package("cachy", "0.2.0") cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) installed.add_package(cachy2) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.2.0", "description": "", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"msgpack-python": ">=0.5 <0.6"}, }, { "name": "msgpack-python", "version": "0.5.1", "description": "", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "msgpack-python": []}, }, } ) tester.execute("--tree", supports_utf8=False) expected = """\ cachy 0.2.0 `-- msgpack-python >=0.5 <0.6 """ assert tester.io.fetch_output() == expected def test_show_tree_no_dev( tester: CommandTester, poetry: Poetry, installed: Repository ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) poetry.package.add_dependency( Factory.create_dependency("pytest", "^6.1.0", groups=["dev"]) ) cachy2 = get_package("cachy", "0.2.0") cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) installed.add_package(cachy2) pytest = get_package("pytest", "6.1.1") installed.add_package(pytest) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.2.0", "description": "", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"msgpack-python": ">=0.5 <0.6"}, }, { "name": "msgpack-python", "version": "0.5.1", "description": "", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "pytest", "version": "6.1.1", "description": "", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "msgpack-python": [], "pytest": []}, }, } ) tester.execute("--tree --without dev") expected = """\ cachy 0.2.0 └── msgpack-python >=0.5 <0.6 """ assert tester.io.fetch_output() == expected def test_show_tree_why_package( tester: CommandTester, poetry: Poetry, installed: Repository ) -> None: poetry.package.add_dependency(Factory.create_dependency("a", "=0.0.1")) a = get_package("a", "0.0.1") installed.add_package(a) a.add_dependency(Factory.create_dependency("b", "=0.0.1")) b = get_package("b", "0.0.1") a.add_dependency(Factory.create_dependency("c", "=0.0.1")) installed.add_package(b) c = get_package("c", "0.0.1") installed.add_package(c) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "a", "version": "0.0.1", "dependencies": {"b": "=0.0.1"}, "python-versions": "*", "optional": False, }, { "name": "b", "version": "0.0.1", "dependencies": {"c": "=0.0.1"}, "python-versions": "*", "optional": False, }, { "name": "c", "version": "0.0.1", "python-versions": "*", "optional": False, }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"a": [], "b": [], "c": []}, }, } ) tester.execute("--tree --why b") expected = """\ a 0.0.1 └── b =0.0.1 └── c =0.0.1 \n""" assert tester.io.fetch_output() == expected def test_show_tree_why( tester: CommandTester, poetry: Poetry, installed: Repository ) -> None: poetry.package.add_dependency(Factory.create_dependency("a", "=0.0.1")) a = get_package("a", "0.0.1") installed.add_package(a) a.add_dependency(Factory.create_dependency("b", "=0.0.1")) b = get_package("b", "0.0.1") b.add_dependency(Factory.create_dependency("c", "=0.0.1")) installed.add_package(b) c = get_package("c", "0.0.1") installed.add_package(c) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "a", "version": "0.0.1", "dependencies": {"b": "=0.0.1"}, "python-versions": "*", "optional": False, }, { "name": "b", "version": "0.0.1", "dependencies": {"c": "=0.0.1"}, "python-versions": "*", "optional": False, }, { "name": "c", "version": "0.0.1", "python-versions": "*", "optional": False, }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"a": [], "b": [], "c": []}, }, } ) tester.execute("--why") # this has to be on a single line due to the padding whitespace, which gets stripped # by pre-commit. expected = """a 0.0.1 \nb 0.0.1 from a \nc 0.0.1 from b \n""" assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_why( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository ) -> None: poetry.package.add_dependency(Factory.create_dependency("a", "=0.0.1")) a = get_package("a", "0.0.1") a.description = "Package A" a.add_dependency(Factory.create_dependency("b", "=0.0.1")) a.add_dependency(Factory.create_dependency("c", "=0.0.1")) installed.add_package(a) b = get_package("b", "0.0.1") b.description = "Package B" b.add_dependency(Factory.create_dependency("c", "=0.0.1")) installed.add_package(b) c = get_package("c", "0.0.1") c.description = "Package C" installed.add_package(c) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "a", "version": "0.0.1", "description": "Package A", "dependencies": {"b": "=0.0.1", "c": "=0.0.1"}, "python-versions": "*", "optional": False, }, { "name": "b", "version": "0.0.1", "description": "Package B", "dependencies": {"c": "=0.0.1"}, "python-versions": "*", "optional": False, }, { "name": "c", "version": "0.0.1", "description": "Package C", "python-versions": "*", "optional": False, }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"a": [], "b": [], "c": []}, }, } ) tester.execute(f"--why {output_format}") expected: str | list[dict[str, str | list[str]]] = "" if "json" in output_format: expected = [ { "name": "a", "version": "0.0.1", "description": "Package A", "installed_status": "installed", }, { "name": "b", "version": "0.0.1", "description": "Package B", "installed_status": "installed", "required_by": ["a"], }, { "name": "c", "version": "0.0.1", "description": "Package C", "installed_status": "installed", "required_by": ["a", "b"], }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ a 0.0.1 Package A b 0.0.1 from a Package B c 0.0.1 from a,b Package C """ assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_required_by_deps( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "2.0.0")) cachy2 = get_package("cachy", "0.2.0") cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) pendulum = get_package("pendulum", "2.0.0") pendulum.add_dependency(Factory.create_dependency("CachY", "^0.2.0")) installed.add_package(cachy2) installed.add_package(pendulum) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.2.0", "description": "", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"msgpack-python": ">=0.5 <0.6"}, }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"cachy": ">=0.2.0 <0.3.0"}, }, { "name": "msgpack-python", "version": "0.5.1", "description": "", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "pendulum": [], "msgpack-python": []}, }, } ) tester.execute(f"cachy {output_format}") expected: str | dict[str, str | dict[str, str]] = "" if "json" in output_format: expected = { "name": "cachy", "version": "0.2.0", "description": "", "dependencies": {"msgpack-python": ">=0.5 <0.6"}, "required_by": {"pendulum": ">=0.2.0 <0.3.0"}, } assert json.loads(tester.io.fetch_output()) == expected else: expected = """\ name : cachy version : 0.2.0 description : dependencies - msgpack-python >=0.5 <0.6 required by - pendulum requires >=0.2.0 <0.3.0 """ actual = [line.rstrip() for line in tester.io.fetch_output().splitlines()] assert actual == expected.splitlines() @pytest.mark.parametrize("truncate", [False, True]) def test_show_entire_description_truncate( tester: CommandTester, poetry: Poetry, installed: Repository, truncate: str ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) cachy2 = get_package("cachy", "0.2.0") cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) installed.add_package(cachy2) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.2.0", "description": "This is a veeeeeeeery long description that might be truncated.", "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"msgpack-python": ">=0.5 <0.6"}, }, { "name": "msgpack-python", "version": "0.5.1", "description": "", "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "msgpack-python": []}, }, } ) tester.execute("" if truncate else "--no-truncate") if truncate: expected = """\ cachy 0.2.0 This is a veeeeeeeery long description that might ... msgpack-python (!) 0.5.1""" else: expected = """\ cachy 0.2.0 This is a veeeeeeeery long description that might be truncated. msgpack-python (!) 0.5.1""" assert tester.io.fetch_output().strip() == expected def test_show_errors_without_lock_file(tester: CommandTester, poetry: Poetry) -> None: assert not poetry.locker.lock.exists() tester.execute() expected = "Error: poetry.lock not found. Run `poetry lock` to create it.\n" assert tester.io.fetch_error() == expected assert tester.status_code == 1 @output_format_parametrize def test_show_dependency_installed_from_git_in_dev( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: # Add a regular dependency for a package in main, and a git dependency for the same # package in dev. poetry.package.add_dependency(Factory.create_dependency("demo", "^0.1.1")) poetry.package.add_dependency( Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git"}, groups=["dev"] ) ) demo_011 = get_package("demo", "0.1.1") demo_011.description = "Demo package" repo.add_package(demo_011) pendulum_200 = get_package("pendulum", "2.0.0") pendulum_200.description = "Pendulum package" repo.add_package(pendulum_200) # The git package is the one that gets into the lockfile. assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "demo", "version": "0.1.2", "description": "Demo package", "optional": False, "python-versions": "*", "develop": False, "source": { "type": "git", "reference": MOCK_DEFAULT_GIT_REVISION, "resolved_reference": MOCK_DEFAULT_GIT_REVISION, "url": "https://github.com/demo/demo.git", }, }, { "name": "pendulum", "version": "2.0.0", "description": "Pendulum package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"demo": [], "pendulum": []}, }, } ) # Nothing needs updating, there is no confusion between the git and not-git # packages. tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [] assert json.loads(tester.io.fetch_output()) == expected else: expected = "" assert tester.io.fetch_output() == expected @output_format_parametrize def test_url_dependency_is_not_outdated_by_repository_package( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: demo_url = ( "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" ) poetry.package.add_dependency( Factory.create_dependency( "demo", {"url": demo_url}, ) ) # A newer version of demo is available in the repository. demo_100 = get_package("demo", "1.0.0") repo.add_package(demo_100) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "demo", "version": "0.1.0", "description": "Demo package", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "source": { "type": "url", "url": demo_url, }, } ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "hashes": {"demo": []}, }, } ) # The url dependency on demo is not made outdated by the existence of a newer # version in the repository. tester.execute(f"--outdated {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [] assert json.loads(tester.io.fetch_output()) == expected else: expected = "" assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_top_level( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) cachy2 = get_package("cachy", "0.2.0") cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) installed.add_package(cachy2) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "cachy", "version": "0.2.0", "description": "", "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"msgpack-python": ">=0.5 <0.6"}, }, { "name": "msgpack-python", "version": "0.5.1", "description": "", "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"cachy": [], "msgpack-python": []}, }, } ) tester.execute(f"--top-level {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "cachy", "version": "0.2.0", "description": "", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """cachy 0.2.0 \n""" assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_top_level_with_explicitly_defined_dependency( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: poetry.package.add_dependency(Factory.create_dependency("a", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("b", "^0.2.0")) a = get_package("a", "0.1.0") a.add_dependency(Factory.create_dependency("b", "0.2.0")) b = get_package("b", "0.2.0") installed.add_package(a) installed.add_package(b) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "a", "version": "0.1.0", "description": "", "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"b": "0.2.0"}, }, { "name": "b", "version": "0.2.0", "description": "", "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"a": [], "b": []}, }, } ) tester.execute(f"--top-level {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "a", "version": "0.1.0", "description": "", "installed_status": "installed", }, { "name": "b", "version": "0.2.0", "description": "", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """a 0.1.0 \nb 0.2.0 \n""" assert tester.io.fetch_output() == expected @output_format_parametrize def test_show_top_level_with_extras( output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository, ) -> None: black_dep = Factory.create_dependency( "black", {"version": "23.3.0", "extras": ["d"]} ) poetry.package.add_dependency(black_dep) black_package = get_package("black", "23.3.0") black_package.add_dependency( Factory.create_dependency( "aiohttp", { "version": ">=3.7.4", "optional": True, "markers": 'extra == "d"', }, ) ) installed.add_package(black_package) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data( { "package": [ { "name": "black", "version": "23.3.0", "description": "", "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": { "aiohttp": { "version": ">=3.7.4", "optional": True, "markers": 'extra == "d"', } }, }, { "name": "aiohttp", "version": "3.8.4", "description": "", "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"black": [], "aiohttp": []}, }, } ) tester.execute(f"--top-level {output_format}") expected: str | list[dict[str, str]] = "" if "json" in output_format: expected = [ { "name": "black", "version": "23.3.0", "description": "", "installed_status": "installed", }, ] assert json.loads(tester.io.fetch_output()) == expected else: expected = """black 23.3.0 \n""" assert tester.io.fetch_output() == expected def test_show_error_top_level_with_tree(tester: CommandTester) -> None: expected = "Error: Cannot use --tree and --top-level at the same time.\n" tester.execute("--top-level --tree") assert tester.io.fetch_error() == expected assert tester.status_code == 1 def test_show_error_top_level_with_single_package(tester: CommandTester) -> None: expected = "Error: Cannot use --top-level when displaying a single package.\n" tester.execute("--top-level some_package_name") assert tester.io.fetch_error() == expected assert tester.status_code == 1 @pytest.mark.parametrize( ("project_directory", "required_fixtures"), [ ( "deleted_directory_dependency", [], ), ], ) def test_show_outdated_missing_directory_dependency( tester: CommandTester, poetry: Poetry, installed: Repository, repo: TestRepository, ) -> None: with (poetry.pyproject.file.path.parent / "poetry.lock").open(mode="rb") as f: data = tomllib.load(f) assert isinstance(poetry.locker, TestLocker) poetry.locker.mock_lock_data(data) poetry.package.add_dependency( Factory.create_dependency( "missing", {"path": data["package"][0]["source"]["url"]}, ) ) with pytest.raises(ValueError, match="does not exist"): tester.execute("") def test_show_error_invalid_output_format( tester: CommandTester, ) -> None: expected = "Error: Invalid output format. Supported formats are: json, text.\n" tester.execute("--format invalid") assert tester.io.fetch_error() == expected assert tester.status_code == 1 def test_show_error_invalid_output_format_with_tree_option( tester: CommandTester, ) -> None: expected = "Error: --tree option can only be used with the text output option.\n" tester.execute("--format json --tree") assert tester.io.fetch_error() == expected assert tester.status_code == 1 ================================================ FILE: tests/console/commands/test_sync.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.exceptions import CleoNoSuchOptionError from poetry.console.commands.sync import SyncCommand # import all tests from the install command # and run them for sync by overriding the command fixture from tests.console.commands.test_install import * # noqa: F403 if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture @pytest.fixture # type: ignore[no-redef] def command() -> str: return "sync" @pytest.mark.skip("Only relevant for `poetry install`") # type: ignore[no-redef] def test_sync_option_is_passed_to_the_installer() -> None: """The only test from the install command that does not work for sync.""" def test_sync_option_not_available(tester: CommandTester) -> None: with pytest.raises(CleoNoSuchOptionError): tester.execute("--sync") def test_synced_installer(tester: CommandTester, mocker: MockerFixture) -> None: assert isinstance(tester.command, SyncCommand) mock = mocker.patch( "poetry.console.commands.install.InstallCommand.installer", new_callable=mocker.PropertyMock, ) tester.execute() mock.return_value.requires_synchronization.assert_called_with(True) ================================================ FILE: tests/console/commands/test_update.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.console.commands.update import UpdateCommand from tests.helpers import get_package if TYPE_CHECKING: from pytest_mock import MockerFixture from poetry.poetry import Poetry from tests.helpers import TestRepository from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture def poetry_with_outdated_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: source = fixture_dir("outdated_lock") return project_factory( name="foobar", pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), poetry_lock_content=(source / "poetry.lock").read_text(encoding="utf-8"), ) @pytest.mark.parametrize( "command", [ "--dry-run", "docker --dry-run", ], ) def test_update_with_dry_run_keep_files_intact( command: str, poetry_with_outdated_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile) original_pyproject_content = poetry_with_outdated_lockfile.file.read() original_lockfile_content = poetry_with_outdated_lockfile._locker.lock_data repo.add_package(get_package("docker", "4.3.0")) repo.add_package(get_package("docker", "4.3.1")) tester.execute(command) assert poetry_with_outdated_lockfile.file.read() == original_pyproject_content assert poetry_with_outdated_lockfile._locker.lock_data == original_lockfile_content @pytest.mark.parametrize( ("command", "expected"), [ ("", True), ("--dry-run", True), ("--lock", False), ], ) def test_update_prints_operations( command: str, expected: bool, poetry_with_outdated_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile) repo.add_package(get_package("docker", "4.3.0")) repo.add_package(get_package("docker", "4.3.1")) tester.execute(command) output = tester.io.fetch_output() assert ("Package operations:" in output) is expected assert ("Installing docker (4.3.1)" in output) is expected def test_update_sync_option_is_passed_to_the_installer( poetry_with_outdated_lockfile: Poetry, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, ) -> None: """ The --sync option is passed properly to the installer from update. """ tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile) assert isinstance(tester.command, UpdateCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) tester.execute("--sync") assert tester.command.installer._requires_synchronization def test_update_with_valid_package_name( poetry_with_outdated_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, ) -> None: """ Specifying a valid dependency should not raise an error. """ tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile) assert isinstance(tester.command, UpdateCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) repo.add_package(get_package("docker", "4.3.1")) status = tester.execute("docker") assert status == 0 assert tester.io.fetch_error() == "" def test_update_with_non_normalized_package_name( poetry_with_outdated_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, ) -> None: """ Package names that differ only in normalization (e.g. 'Docker' vs 'docker') should be accepted. """ tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile) assert isinstance(tester.command, UpdateCommand) mocker.patch.object(tester.command.installer, "run", return_value=0) repo.add_package(get_package("docker", "4.3.1")) status = tester.execute("Docker") assert status == 0 assert tester.io.fetch_error() == "" def test_update_with_invalid_package_name_shows_error( poetry_with_outdated_lockfile: Poetry, command_tester_factory: CommandTesterFactory, ) -> None: """ Providing non-existent package names should raise an error. """ tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile) status = tester.execute("nonexistent-package") assert status == 1 assert ( "The following packages are not dependencies of this project: nonexistent-package" in tester.io.fetch_error() ) def test_update_with_multiple_invalid_package_names_shows_error( poetry_with_outdated_lockfile: Poetry, command_tester_factory: CommandTesterFactory, ) -> None: """ Providing multiple non-existent package names should list all of them in the error. """ tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile) status = tester.execute("fake1 fake2 fake3") assert status == 1 error = tester.io.fetch_error() assert "The following packages are not dependencies of this project" in error assert "fake1" in error assert "fake2" in error assert "fake3" in error @pytest.mark.parametrize( "package_spec", [ "docker==1.2.3", "docker>=1.0,<2.0", "docker!=1.0", "docker[extra]>=1.0", "nonexistent==1.2.3", "nonexistent>=1.0", ], ) def test_update_with_version_specifier_raises_error( package_spec: str, poetry_with_outdated_lockfile: Poetry, command_tester_factory: CommandTesterFactory, ) -> None: """ The update command only accepts bare package names. Passing requirement strings with version specifiers should raise a clear error pointing to poetry add, regardless of whether the package is a dependency. """ tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile) status = tester.execute(package_spec) assert status == 1 error = tester.io.fetch_error() assert "Version specifiers are not allowed" in error assert "poetry update" in error assert "poetry add" in error ================================================ FILE: tests/console/commands/test_version.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.console.commands.version import VersionCommand if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester from poetry.poetry import Poetry from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture() def command() -> VersionCommand: return VersionCommand() @pytest.fixture def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("version") @pytest.fixture def poetry_with_underscore( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter ) -> Poetry: source = fixture_dir("simple_project") pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") pyproject_content = pyproject_content.replace("simple-project", "simple_project") return project_factory( "project_with_underscore", pyproject_content=pyproject_content ) @pytest.mark.parametrize( "version, rule, expected", [ ("0.0.0", "patch", "0.0.1"), ("0.0.0", "minor", "0.1.0"), ("0.0.0", "major", "1.0.0"), ("0.0", "major", "1.0"), ("0.0", "minor", "0.1"), ("0.0", "patch", "0.0.1"), ("1.2.3", "patch", "1.2.4"), ("1.2.3", "minor", "1.3.0"), ("1.2.3", "major", "2.0.0"), ("1.2.3", "prepatch", "1.2.4a0"), ("1.2.3", "preminor", "1.3.0a0"), ("1.2.3", "premajor", "2.0.0a0"), ("1.2.3-beta.1", "patch", "1.2.3"), ("1.2.3-beta.1", "minor", "1.3.0"), ("1.2.3-beta.1", "major", "2.0.0"), ("1.2.3-beta.1", "prerelease", "1.2.3b2"), ("1.2.3-beta1", "prerelease", "1.2.3b2"), ("1.2.3beta1", "prerelease", "1.2.3b2"), ("1.2.3b1", "prerelease", "1.2.3b2"), ("1.2.3", "prerelease", "1.2.4a0"), ("0.0.0", "1.2.3", "1.2.3"), ], ) def test_increment_version( version: str, rule: str, expected: str, command: VersionCommand ) -> None: assert command.increment_version(version, rule).text == expected @pytest.mark.parametrize( "version, rule, expected", [ ("1.2.3", "prerelease", "1.2.4a0"), ("1.2.3a0", "prerelease", "1.2.3b0"), ("1.2.3a1", "prerelease", "1.2.3b0"), ("1.2.3b1", "prerelease", "1.2.3rc0"), ("1.2.3rc0", "prerelease", "1.2.3"), ("1.2.3-beta.1", "prerelease", "1.2.3rc0"), ("1.2.3-beta1", "prerelease", "1.2.3rc0"), ("1.2.3beta1", "prerelease", "1.2.3rc0"), ], ) def test_next_phase_version( version: str, rule: str, expected: str, command: VersionCommand ) -> None: assert command.increment_version(version, rule, True).text == expected def test_version_show(tester: CommandTester) -> None: tester.execute() assert tester.io.fetch_output() == "simple-project 1.2.3\n" def test_version_show_with_underscore( command_tester_factory: CommandTesterFactory, poetry_with_underscore: Poetry ) -> None: tester = command_tester_factory("version", poetry=poetry_with_underscore) tester.execute() assert tester.io.fetch_output() == "simple_project 1.2.3\n" def test_short_version_show(tester: CommandTester) -> None: tester.execute("--short") assert tester.io.fetch_output() == "1.2.3\n" def test_version_update(tester: CommandTester) -> None: tester.execute("2.0.0") assert tester.io.fetch_output() == "Bumping version from 1.2.3 to 2.0.0\n" def test_short_version_update(tester: CommandTester) -> None: tester.execute("--short 2.0.0") assert tester.io.fetch_output() == "2.0.0\n" def test_phase_version_update(tester: CommandTester) -> None: assert isinstance(tester.command, VersionCommand) tester.command.poetry.package._set_version("1.2.4a0") tester.execute("prerelease --next-phase") assert tester.io.fetch_output() == "Bumping version from 1.2.4a0 to 1.2.4b0\n" def test_dry_run(tester: CommandTester) -> None: assert isinstance(tester.command, VersionCommand) old_pyproject = tester.command.poetry.file.path.read_text(encoding="utf-8") tester.execute("--dry-run major") new_pyproject = tester.command.poetry.file.path.read_text(encoding="utf-8") assert tester.io.fetch_output() == "Bumping version from 1.2.3 to 2.0.0\n" assert old_pyproject == new_pyproject ================================================ FILE: tests/console/conftest.py ================================================ from __future__ import annotations import os from typing import TYPE_CHECKING import pytest from cleo.io.null_io import NullIO from cleo.testers.application_tester import ApplicationTester from cleo.testers.command_tester import CommandTester from poetry.installation import Installer from poetry.utils.env import MockEnv from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import PoetryTestApplication from tests.helpers import TestExecutor from tests.helpers import mock_clone if TYPE_CHECKING: from collections.abc import Iterator from pathlib import Path from pytest_mock import MockerFixture from poetry.installation.executor import Executor from poetry.poetry import Poetry from poetry.repositories import Repository from poetry.utils.env import Env from tests.conftest import Config from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture def env(tmp_path: Path) -> MockEnv: path = tmp_path / ".venv" path.mkdir(parents=True) return MockEnv(path=path, is_venv=True) @pytest.fixture(autouse=True) def setup( mocker: MockerFixture, installed: Repository, config: Config, env: MockEnv, ) -> Iterator[None]: # Do not run pip commands of the executor mocker.patch("poetry.installation.executor.Executor.run_pip") p = mocker.patch("poetry.installation.installer.Installer._get_installed") p.return_value = installed p = mocker.patch( "poetry.repositories.installed_repository.InstalledRepository.load" ) p.return_value = installed # Patch git module to not actually clone projects mocker.patch("poetry.vcs.git.Git.clone", new=mock_clone) p = mocker.patch("poetry.vcs.git.Git.get_revision") p.return_value = MOCK_DEFAULT_GIT_REVISION # Patch the virtual environment creation do actually do nothing mocker.patch("poetry.utils.env.EnvManager.create_venv", return_value=env) # Patch the virtual environment creation do actually do nothing mocker.patch("poetry.utils.env.EnvManager.create_venv", return_value=env) # Setting terminal width environ = dict(os.environ) os.environ["COLUMNS"] = "80" yield os.environ.clear() os.environ.update(environ) @pytest.fixture def project_directory() -> str: return "simple_project" @pytest.fixture def poetry( project_directory: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, ) -> Poetry: return project_factory(name="simple", source=fixture_dir(project_directory)) @pytest.fixture def app(poetry: Poetry) -> PoetryTestApplication: app_ = PoetryTestApplication(poetry) io = NullIO() app_._load_plugins(io) return app_ @pytest.fixture def app_tester(app: PoetryTestApplication) -> ApplicationTester: return ApplicationTester(app) @pytest.fixture() def executor(poetry: Poetry, config: Config, env: MockEnv) -> TestExecutor: return TestExecutor(env, poetry.pool, config, NullIO()) @pytest.fixture def command_tester_factory( app: PoetryTestApplication, env: MockEnv ) -> CommandTesterFactory: def _tester( command: str, poetry: Poetry | None = None, installer: Installer | None = None, executor: Executor | None = None, environment: Env | None = None, ) -> CommandTester: command_obj = app.find(command) tester = CommandTester(command_obj) # Setting the formatter from the application # TODO: Find a better way to do this in Cleo app_io = app.create_io() formatter = app_io.output.formatter tester.io.output.set_formatter(formatter) tester.io.error_output.set_formatter(formatter) if poetry: app._poetry = poetry poetry = app.poetry if hasattr(command_obj, "set_env"): command_obj.set_env(environment or env) if hasattr(command_obj, "set_installer"): installer = installer or Installer( tester.io, env, poetry.package, poetry.locker, poetry.pool, poetry.config, executor=executor or TestExecutor(env, poetry.pool, poetry.config, tester.io), ) command_obj.set_installer(installer) return tester return _tester @pytest.fixture def do_lock(command_tester_factory: CommandTesterFactory, poetry: Poetry) -> None: command_tester_factory("lock").execute() assert poetry.locker.lock.exists() ================================================ FILE: tests/console/logging/__init__.py ================================================ ================================================ FILE: tests/console/logging/formatters/__init__.py ================================================ ================================================ FILE: tests/console/logging/formatters/test_builder_formatter.py ================================================ from __future__ import annotations import pytest from poetry.console.logging.formatters.builder_formatter import BuilderLogFormatter @pytest.mark.parametrize( "input_msg, expected_output", [ ("Building package", " - Building package"), ("Built package", " - Built package"), ("Adding: dependency", " - Adding: dependency"), ( "Executing build script: setup.py", " - Executing build script: setup.py", ), ("Some other message", "Some other message"), # No formatting should be applied ("", ""), # Edge case: Empty string ( " Building package ", " Building package ", ), # Edge case: Whitespace handling ("building package", "building package"), # Edge case: Case sensitivity ], ) def test_builder_log_formatter(input_msg: str, expected_output: str) -> None: formatter = BuilderLogFormatter() assert formatter.format(input_msg) == expected_output ================================================ FILE: tests/console/logging/test_io_formatter.py ================================================ from __future__ import annotations from logging import LogRecord from pathlib import Path from typing import TYPE_CHECKING import pytest from poetry.console.logging.io_formatter import IOFormatter from poetry.console.logging.io_formatter import _log_prefix from poetry.console.logging.io_formatter import _path_to_package if TYPE_CHECKING: from pytest_mock import MockerFixture @pytest.mark.parametrize( ("record_name", "record_pathname", "record_msg", "expected"), [ ("poetry", "foo/bar.py", "msg", "msg"), ("poetry.core", "foo/bar.py", "msg", "msg"), ("baz", "syspath/foo/bar.py", "msg", "[foo:baz] msg"), ("root", "syspath/foo/bar.py", "1\n\n2", "[foo] 1\n[foo] \n[foo] 2"), ], ) def test_format( mocker: MockerFixture, record_name: str, record_pathname: str, record_msg: str, expected: str, ) -> None: mocker.patch("sys.path", [str(Path("syspath"))]) record = LogRecord(record_name, 0, record_pathname, 0, record_msg, (), None) formatter = IOFormatter() assert formatter.format(record) == expected @pytest.mark.parametrize( ("record_name", "record_pathname", "expected"), [ ("root", "syspath/foo/bar.py", "foo"), ("baz", "syspath/foo/bar.py", "foo:baz"), ("baz", "unexpected/foo/bar.py", "bar:baz"), ], ) def test_log_prefix( mocker: MockerFixture, record_name: str, record_pathname: str, expected: str, ) -> None: mocker.patch("sys.path", [str(Path("syspath"))]) record = LogRecord(record_name, 0, record_pathname, 0, "msg", (), None) assert _log_prefix(record) == expected @pytest.mark.parametrize( ("path", "expected"), [ ("python-l/lib/python3.9/site-packages/foo/bar/baz.py", "foo"), # Linux ("python-w/lib/site-packages/foo/bar/baz.py", "foo"), # Windows ("unexpected/foo/bar/baz.py", None), # unexpected ], ) def test_path_to_package( mocker: MockerFixture, path: str, expected: str | None ) -> None: mocker.patch( "sys.path", # We just put the Linux and the Windows variants in the path, # so we do not have to create different mocks based on the subtest. [ # On Linux, only the site-packages directory is in the path. str(Path("python-l/lib/python3.9/site-packages")), # On Windows, both the base directory and the site-packages directory # are in the path. str(Path("python-w")), str(Path("python-w/other")), # this one is just to test for robustness str(Path("python-w/lib/site-packages")), str(Path("python-w/lib")), # this one is just to test for robustness ], ) assert _path_to_package(Path(path)) == expected ================================================ FILE: tests/console/test_application.py ================================================ from __future__ import annotations import re import shutil from typing import TYPE_CHECKING from typing import ClassVar from typing import cast import pytest from cleo.testers.application_tester import ApplicationTester from poetry.console.application import Application from poetry.console.commands.command import Command from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin_manager import ProjectPluginCache from poetry.repositories.cached_repository import CachedRepository from poetry.utils.authenticator import Authenticator from poetry.utils.env import EnvManager from poetry.utils.env import MockEnv from tests.helpers import mock_metadata_entry_points if TYPE_CHECKING: from pathlib import Path from cleo.io.inputs.argv_input import ArgvInput from pytest_mock import MockerFixture from tests.helpers import PoetryTestApplication from tests.types import FixtureDirGetter from tests.types import SetProjectContext class FooCommand(Command): name = "foo" description = "Foo Command" def handle(self) -> int: self.line("foo called") return 0 class AddCommandPlugin(ApplicationPlugin): commands: ClassVar[list[type[Command]]] = [FooCommand] @pytest.fixture def with_add_command_plugin(mocker: MockerFixture) -> None: mock_metadata_entry_points(mocker, AddCommandPlugin) def test_application_with_plugins(with_add_command_plugin: None) -> None: app = Application() tester = ApplicationTester(app) tester.execute("") assert re.search(r"\s+foo\s+Foo Command", tester.io.fetch_output()) is not None assert tester.status_code == 0 def test_application_with_plugins_disabled(with_add_command_plugin: None) -> None: app = Application() tester = ApplicationTester(app) tester.execute("--no-plugins") assert re.search(r"\s+foo\s+Foo Command", tester.io.fetch_output()) is None assert tester.status_code == 0 def test_application_execute_plugin_command(with_add_command_plugin: None) -> None: app = Application() tester = ApplicationTester(app) tester.execute("foo") assert tester.io.fetch_output() == "foo called\n" assert tester.status_code == 0 def test_application_execute_plugin_command_with_plugins_disabled( with_add_command_plugin: None, ) -> None: app = Application() tester = ApplicationTester(app) tester.execute("foo --no-plugins") assert tester.io.fetch_output() == "" assert "The requested command foo does not exist." in tester.io.fetch_error() assert tester.status_code == 1 @pytest.mark.parametrize("with_project_plugins", [False, True]) @pytest.mark.parametrize("no_plugins", [False, True]) def test_application_project_plugins( fixture_dir: FixtureDirGetter, tmp_path: Path, no_plugins: bool, with_project_plugins: bool, mocker: MockerFixture, set_project_context: SetProjectContext, ) -> None: env = MockEnv( path=tmp_path / "env", version_info=(3, 8, 0), sys_path=[str(tmp_path / "env")] ) mocker.patch.object(EnvManager, "get_system_env", return_value=env) orig_dir = fixture_dir("project_plugins") project_path = tmp_path / "project" project_path.mkdir() shutil.copy(orig_dir / "pyproject.toml", project_path / "pyproject.toml") project_plugin_path = project_path / ProjectPluginCache.PATH if with_project_plugins: project_plugin_path.mkdir(parents=True) with set_project_context(project_path, in_place=True): app = Application() tester = ApplicationTester(app) tester.execute("--no-plugins" if no_plugins else "") assert tester.status_code == 0 sys_path = EnvManager.get_system_env(naive=True).sys_path if with_project_plugins and not no_plugins: assert sys_path[0] == str(project_plugin_path) else: assert sys_path[0] != str(project_plugin_path) @pytest.mark.parametrize("disable_cache", [True, False]) def test_application_verify_source_cache_flag( disable_cache: bool, set_project_context: SetProjectContext ) -> None: with set_project_context("sample_project"): app = Application() tester = ApplicationTester(app) command = "debug info" if disable_cache: command = f"{command} --no-cache" assert not app._poetry tester.execute(command) assert app.poetry.pool.repositories for repo in app.poetry.pool.repositories: assert isinstance(repo, CachedRepository) assert repo._disable_cache == disable_cache @pytest.mark.parametrize("disable_cache", [True, False]) def test_application_verify_cache_flag_at_install( mocker: MockerFixture, disable_cache: bool, set_project_context: SetProjectContext, ) -> None: import poetry.utils.authenticator # Set default authenticator to None so that it is recreated for each test # and we get a consistent call_count. poetry.utils.authenticator._authenticator = None with set_project_context("sample_project"): app = Application() tester = ApplicationTester(app) command = "install --dry-run" if disable_cache: command = f"{command} --no-cache" spy = mocker.spy(Authenticator, "__init__") tester.execute(command) # The third call is the default authenticator, which ignores the cache flag. assert spy.call_count == 3 for call in spy.mock_calls[:2]: (_name, _args, kwargs) = call assert "disable_cache" in kwargs assert disable_cache is kwargs["disable_cache"] @pytest.mark.parametrize( ("tokens", "result"), [ ( ["-C", "/path/working/dir", "env", "list"], ["--directory", "/path/working/dir", "env", "list"], ), ( ["-P", "/path/project/dir", "env", "list"], ["--project", "/path/project/dir", "env", "list"], ), ( ["-P/path/project/dir", "env", "list"], ["--project", "/path/project/dir", "env", "list"], ), ( ["-P/path/project/dir", "env", "list"], ["--project", "/path/project/dir", "env", "list"], ), ( ["-v", "run", "-P/path/project/dir", "echo", "--help"], [ "--verbose", "--project", "/path/project/dir", "run", "--", "echo", "--help", ], ), ( ["--no-ansi", "run", "-V", "python", "-V"], ["--version", "--no-ansi", "run", "--", "python", "-V"], ), ( ["--no-ansi", "run", "-V", "--", "python", "-V"], ["--version", "--no-ansi", "run", "--", "python", "-V"], ), ], ) def test_application_input_configuration_and_sorting( tokens: list[str], result: list[str], app: PoetryTestApplication ) -> None: app.create_io() assert app._io is not None io_input = cast("ArgvInput", app._io.input) io_input._tokens = tokens app._configure_io(app._io) app._sort_global_options(app._io) io_input = cast("ArgvInput", app._io.input) assert io_input._tokens == result ================================================ FILE: tests/console/test_application_command_not_found.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.testers.application_tester import ApplicationTester from poetry.console.application import Application if TYPE_CHECKING: from tests.types import CommandFactory @pytest.fixture def tester() -> ApplicationTester: return ApplicationTester(Application()) @pytest.mark.parametrize( ("command", "suggested"), [ ("x", None), ("en", ["env activate", "env info", "env list", "env remove", "env use"]), ("sou", ["source add", "source remove", "source show"]), ], ) def test_application_command_not_found_messages( command: str, suggested: list[str] | None, tester: ApplicationTester, command_factory: CommandFactory, ) -> None: tester.execute(f"{command}") assert tester.status_code != 0 stderr = tester.io.fetch_error() assert f"The requested command {command} does not exist." in stderr if suggested is None: assert "Did you mean one of these perhaps?" not in stderr else: for suggestion in suggested: assert suggestion in stderr @pytest.mark.parametrize( "namespace", ["cache", "debug", "env", "self", "source"], ) def test_application_namespaced_command_not_found_messages( namespace: str, tester: ApplicationTester, command_factory: CommandFactory, ) -> None: tester.execute(f"{namespace} xxx") assert tester.status_code != 0 stderr = tester.io.fetch_error() assert ( f"The requested command does not exist in the {namespace} namespace." in stderr ) ================================================ FILE: tests/console/test_application_global_options.py ================================================ from __future__ import annotations import re import textwrap from pathlib import Path from typing import TYPE_CHECKING from typing import ClassVar import pytest from cleo.io.buffered_io import BufferedIO from cleo.io.inputs.string_input import StringInput from cleo.testers.application_tester import ApplicationTester from poetry.console.application import Application from poetry.console.commands.command import Command from poetry.console.commands.version import VersionCommand from poetry.plugins import ApplicationPlugin from tests.helpers import mock_metadata_entry_points from tests.helpers import switch_working_directory if TYPE_CHECKING: from pytest import TempPathFactory from pytest_mock import MockerFixture from tests.types import FixtureCopier NO_PYPROJECT_TOML_ERROR = "Poetry could not find a pyproject.toml file in" class CheckProjectPathCommand(Command): name = "check-project-path" description = "Check Project Path Command" def handle(self) -> int: if not self.poetry.pyproject_path.exists(): raise RuntimeError( f"Wrong project path in handle: {self.poetry.pyproject_path}\nWorking directory: {Path.cwd()}" ) return 0 class EarlyPoetryAccessPlugin(ApplicationPlugin): commands: ClassVar[list[type[Command]]] = [CheckProjectPathCommand] def activate(self, application: Application) -> None: super().activate(application) # access application.poetry # see https://github.com/nat-n/poethepoet/issues/288 if not application.poetry.pyproject_path.exists(): raise RuntimeError( f"Wrong project path in activate: {application.poetry.pyproject_path}\nWorking directory: {Path.cwd()}" ) @pytest.fixture def with_early_poetry_access_plugin(mocker: MockerFixture) -> None: mock_metadata_entry_points(mocker, EarlyPoetryAccessPlugin) @pytest.fixture def project_source_directory(fixture_copier: FixtureCopier) -> Path: return fixture_copier("up_to_date_lock") @pytest.fixture def relative_project_source_directory(project_source_directory: Path) -> Path: # ensure pre-conditions are met cwd = Path.cwd() assert project_source_directory.is_relative_to(cwd) # construct relative path relative_source_directory = project_source_directory.relative_to(cwd) assert relative_source_directory.as_posix() != project_source_directory.as_posix() assert not relative_source_directory.is_absolute() return relative_source_directory @pytest.fixture def tester() -> ApplicationTester: return ApplicationTester(Application()) @pytest.fixture def with_mocked_version_command(mocker: MockerFixture) -> None: orig_version_command = VersionCommand.handle def mock_handle(command: VersionCommand) -> int: exit_code = orig_version_command(command) command.io.write_line(f"ProjectPath: {command.poetry.pyproject_path.parent}") command.io.write_line(f"WorkingDirectory: {Path.cwd()}") return exit_code mocker.patch("poetry.console.commands.version.VersionCommand.handle", mock_handle) def test_application_global_option_ensure_error_when_context_invalid( tester: ApplicationTester, ) -> None: # command fails due to lack of pyproject.toml file in cwd tester.execute("show --only main") assert tester.status_code != 0 stderr = tester.io.fetch_error() assert NO_PYPROJECT_TOML_ERROR in stderr @pytest.mark.parametrize("parameter", ["-C", "--directory", "-P", "--project"]) @pytest.mark.parametrize( "command_args", [ "{option} show --only main", "show {option} --only main", "show --only main {option}", ], ) def test_application_global_option_position_does_not_matter( parameter: str, command_args: str, tester: ApplicationTester, project_source_directory: Path, ) -> None: cwd = Path.cwd() assert cwd != project_source_directory option = f"{parameter} {project_source_directory.as_posix()}" tester.execute(command_args.format(option=option)) assert tester.status_code == 0 stdout = tester.io.fetch_output() stderr = tester.io.fetch_error() assert NO_PYPROJECT_TOML_ERROR not in stderr assert NO_PYPROJECT_TOML_ERROR not in stdout assert "certifi" in stdout assert len(stdout.splitlines()) == 8 @pytest.mark.parametrize("parameter", ["-C", "--directory", "-P", "--project"]) @pytest.mark.parametrize( "invalid_source_directory", [ "/invalid/path", # non-existent path __file__, # not a directory ], ) def test_application_global_option_context_is_validated( parameter: str, tester: ApplicationTester, invalid_source_directory: str, ) -> None: option = f"{parameter} '{invalid_source_directory}'" tester.execute(f"show {option}") assert tester.status_code != 0 stdout = tester.io.fetch_output() assert stdout == "" stderr = tester.io.fetch_error() assert re.match( r"\nSpecified path '(.+)?' is not a valid directory.\n", stderr, ) @pytest.mark.parametrize("parameter", ["project", "directory"]) def test_application_with_context_parameters( parameter: str, tester: ApplicationTester, project_source_directory: Path, with_mocked_version_command: None, ) -> None: # ensure pre-conditions are met assert project_source_directory != Path.cwd() is_directory_param = parameter == "directory" tester.execute(f"--{parameter} {project_source_directory} version") assert tester.io.fetch_error() == "" assert tester.status_code == 0 output = tester.io.fetch_output() assert output == textwrap.dedent(f"""\ foobar 0.1.0 ProjectPath: {project_source_directory} WorkingDirectory: {project_source_directory if is_directory_param else Path.cwd()} """) def test_application_with_relative_project_parameter( tester: ApplicationTester, project_source_directory: Path, relative_project_source_directory: Path, with_mocked_version_command: None, tmp_path_factory: TempPathFactory, ) -> None: cwd = Path.cwd() # we expect application run to be executed within current cwd # but project to be a subdirectory args = f"--directory '{cwd}' --project {relative_project_source_directory} version" # we switch cwd to a new temporary directory unrelated to the project directory new_working_dir = tmp_path_factory.mktemp("unrelated-working-directory") with switch_working_directory(new_working_dir): assert Path.cwd() == new_working_dir tester.execute(args) assert tester.io.fetch_error() == "" assert tester.status_code == 0 output = tester.io.fetch_output() assert output == textwrap.dedent(f"""\ foobar 0.1.0 ProjectPath: {project_source_directory} WorkingDirectory: {cwd} """) def test_application_with_relative_directory_parameter_and_early_poetry_access_plugin( tester: ApplicationTester, with_early_poetry_access_plugin: None, relative_project_source_directory: Path, ) -> None: """see https://github.com/nat-n/poethepoet/issues/288""" tester.execute( f"--directory {relative_project_source_directory} check-project-path" ) assert tester.status_code == 0, tester.io.fetch_error() @pytest.mark.parametrize( ("parameter", "check", "result"), [ ("--ansi", "is_decorated", True), ("--no-ansi", "is_decorated", False), ("--no-interaction", "is_interactive", False), ("--verbose", "is_verbose", True), ("-vv", "is_verbose", True), ("-vv", "is_very_verbose", True), ("-vv", "is_debug", False), ("-vvv", "is_debug", True), ], ) def test_application_io_options_are_set( parameter: str, check: str, result: bool ) -> None: # we use an actual application here to avoid cleo's testing overrides application = Application() application.auto_exits(False) application._io = BufferedIO() assert application.run(StringInput(f"{parameter} about")) == 0 assert getattr(application._io, check)() == result ================================================ FILE: tests/console/test_application_removed_commands.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.testers.application_tester import ApplicationTester from poetry.console.application import COMMAND_NOT_FOUND_PREFIX_MESSAGE from poetry.console.application import Application if TYPE_CHECKING: from tests.types import CommandFactory @pytest.fixture def tester() -> ApplicationTester: return ApplicationTester(Application()) def test_application_removed_command_default_message( tester: ApplicationTester, ) -> None: tester.execute("nonexistent") assert tester.status_code != 0 stderr = tester.io.fetch_error() assert COMMAND_NOT_FOUND_PREFIX_MESSAGE not in stderr assert "The requested command nonexistent does not exist." in stderr @pytest.mark.parametrize( ("command", "message"), [ ("shell", "shell command is not installed by default"), ], ) def test_application_removed_command_messages( command: str, message: str, tester: ApplicationTester, command_factory: CommandFactory, ) -> None: # ensure precondition is met assert not tester.application.has(command) # verify that the custom message is returned and command fails tester.execute(command) assert tester.status_code != 0 stderr = tester.io.fetch_error() assert COMMAND_NOT_FOUND_PREFIX_MESSAGE in stderr assert message in stderr # flush any output/error messages to ensure consistency tester.io.clear() # add a mock command and verify the command succeeds and no error message is provided message = "The shell command was called" tester.application.add(command_factory(command, command_handler=message)) assert tester.application.has(command) tester.execute(command) assert tester.status_code == 0 stdout = tester.io.fetch_output() stderr = tester.io.fetch_error() assert message in stdout assert COMMAND_NOT_FOUND_PREFIX_MESSAGE not in stderr assert stderr == "" ================================================ FILE: tests/console/test_exceptions_console_message.py ================================================ from __future__ import annotations import pytest from poetry.console.exceptions import ConsoleMessage @pytest.mark.parametrize( ("text", "expected_stripped"), [ ("Hello, World!", "Hello, World!"), ("Bold", "Bold"), ("Italic", "Italic"), ], ) def test_stripped_property(text: str, expected_stripped: str) -> None: """Test the stripped property with various tagged inputs.""" message = ConsoleMessage(text) assert message.stripped == expected_stripped @pytest.mark.parametrize( ("text", "tag", "expected"), [ ("Hello, World!", "info", "Hello, World!"), ("Error occurred", "error", "Error occurred"), ("", "info", ""), # Test with empty input ], ) def test_wrap(text: str, tag: str, expected: str) -> None: """Test the wrap method with various inputs.""" message = ConsoleMessage(text) assert message.wrap(tag).text == expected @pytest.mark.parametrize( ("text", "indent", "expected"), [ ("Hello, World!", " ", " Hello, World!"), ("Line 1\nLine 2", ">>", ">>Line 1\n>>Line 2"), ("", " ", ""), # Test with empty input (" ", " ", " "), # Test with whitespace input ], ) def test_indent(text: str, indent: str, expected: str) -> None: """Test the indent method with various inputs.""" message = ConsoleMessage(text) assert message.indent(indent).text == expected @pytest.mark.parametrize( ("text", "title", "indent", "expected"), [ ("Hello, World!", "Greeting", "", "Greeting:\nHello, World!"), ( "This is a message.", "Section Title", " ", "Section Title:\n This is a message.", ), ("", "Title", "", ""), # Test with empty text ("Multi-line\nText", "Title", ">>>", "Title:\n>>>Multi-line\n>>>Text"), ], ) def test_make_section(text: str, title: str, indent: str, expected: str) -> None: """Test the make_section method with various inputs.""" message = ConsoleMessage(text) assert message.make_section(title, indent).text == expected ================================================ FILE: tests/console/test_exections_poetry_runtime_error.py ================================================ from __future__ import annotations from subprocess import CalledProcessError import pytest from poetry.console.exceptions import ConsoleMessage from poetry.console.exceptions import PoetryRuntimeError @pytest.mark.parametrize( ("reason", "messages", "exit_code", "expected_reason"), [ ("Error occurred!", None, 1, "Error occurred!"), # Default scenario ( "Specific error", [ConsoleMessage("Additional details.")], 2, "Specific error", ), # Custom exit code and messages ("Minimal error", [], 0, "Minimal error"), # No additional messages ], ) def test_poetry_runtime_error_init( reason: str, messages: list[ConsoleMessage] | None, exit_code: int, expected_reason: str, ) -> None: """Test the basic initialization of the PoetryRuntimeError class.""" error = PoetryRuntimeError(reason, messages, exit_code) assert error.exit_code == exit_code assert str(error) == expected_reason assert isinstance(error._messages[0], ConsoleMessage) assert error._messages[0].text == reason @pytest.mark.parametrize( ("debug", "strip", "indent", "messages", "expected_text"), [ ( False, False, "", [ ConsoleMessage("Basic message"), ConsoleMessage("Debug message", debug=True), ], "Error\n\nBasic message\n\nYou can also run your poetry command with -v to see more information.", ), # Debug message ignored ( True, False, "", [ ConsoleMessage("Info message"), ConsoleMessage("Debug message", debug=True), ], "Error\n\nInfo message\n\nDebug message", ), # Debug message included in verbose mode ( True, True, "", [ ConsoleMessage("Bolded message"), ConsoleMessage("Debug Italics Message", debug=True), ], "Error\n\nBolded message\n\nDebug Italics Message", ), # Stripped tags and debug message ( False, False, " ", [ConsoleMessage("Error occurred!")], " Error\n \n Error occurred!", ), # Indented message ], ) def test_poetry_runtime_error_get_text( debug: bool, strip: bool, indent: str, messages: list[ConsoleMessage], expected_text: str, ) -> None: """Test the get_text method of PoetryRuntimeError.""" error = PoetryRuntimeError("Error", messages) text = error.get_text(debug=debug, strip=strip, indent=indent) assert text == expected_text @pytest.mark.parametrize( ("reason", "exception", "info", "expected_message_texts"), [ ( "Command failed", None, None, ["Command failed", ""], # No exception or additional info ), ( "Command failure", Exception("An exception occurred"), None, [ "Command failure", "Exception:\n | An exception occurred", "", ], # Exception message included ), ( "Subprocess error", CalledProcessError(1, ["cmd"], b"stdout", b"stderr"), ["Additional info"], [ "Subprocess error", "Exception:\n" " | Command '['cmd']' returned non-zero exit status 1.", "Output:\n | stdout", "Errors:\n | stderr", "Additional info", "You can test the failed command by executing:\n\n cmd", ], ), ], ) def test_poetry_runtime_error_create( reason: str, exception: Exception, info: list[str], expected_message_texts: list[str], ) -> None: """Test the create class method of PoetryRuntimeError.""" error = PoetryRuntimeError.create(reason, exception, info) assert isinstance(error, PoetryRuntimeError) assert all(isinstance(msg, ConsoleMessage) for msg in error._messages) actual_texts = [msg.text for msg in error._messages] assert actual_texts == expected_message_texts def test_poetry_runtime_error_append() -> None: """Test the append method of PoetryRuntimeError.""" error = PoetryRuntimeError.create("Error", info=["Hello"]).append("World") actual_texts = [msg.text for msg in error._messages] assert actual_texts == ["Error", "Hello", "World"] ================================================ FILE: tests/fixtures/bad_scripts_project/no_colon/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/bad_scripts_project/no_colon/pyproject.toml ================================================ [tool.poetry] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = ["README.rst"] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" [tool.poetry.scripts] foo = "bar.bin.foo" [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/bad_scripts_project/no_colon/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/bad_scripts_project/too_many_colon/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/bad_scripts_project/too_many_colon/pyproject.toml ================================================ [tool.poetry] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = ["README.rst"] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" [tool.poetry.scripts] foo = "foo::bar" [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/bad_scripts_project/too_many_colon/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_constraints/pyproject.toml ================================================ [project] name = "build-constraints" version = "0.1.0" [tool.poetry.build-constraints] Legacy-Lib = { setuptools = "<75" } no-constraints = {} [tool.poetry.build-constraints.c-ext-lib] Cython = { version = "<3.1", source = "pypi" } setuptools = [ { version = ">=60,<75", python = "<3.9" }, { version = ">=75", python = ">=3.8" } ] ================================================ FILE: tests/fixtures/build_constraints_empty/pyproject.toml ================================================ [project] name = "build-constraints" version = "0.1.0" [tool.poetry.build-constraints] ================================================ FILE: tests/fixtures/build_system_requires_not_available/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_system_requires_not_available/pyproject.toml ================================================ [tool.poetry] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = ["README.rst"] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "^3.7" [build-system] requires = ["poetry-core==0.999"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/build_system_requires_not_available/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_systems/core_from_git/README.md ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_systems/core_from_git/pyproject.toml ================================================ [project] name = "core-from-git" version = "1.2.3" description = "Some description." authors = [ { name = "Poetry Contributors", email = "no-reply@python-poetry.org" } ] license = { text = "MIT" } readme = "README.md" keywords = ["packaging", "dependency", "poetry"] requires-python = ">=3.4" [build-system] requires = ["poetry-core @ git+https://github.com/python-poetry/poetry-core.git"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/build_systems/core_from_git/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_systems/core_in_range/README.md ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_systems/core_in_range/pyproject.toml ================================================ [project] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ { name = "Poetry Contributors", email = "no-reply@python-poetry.org" } ] license = { text = "MIT" } readme = "README.md" keywords = ["packaging", "dependency", "poetry"] requires-python = ">=3.4" [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/build_systems/core_in_range/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_systems/core_not_in_range/README.md ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_systems/core_not_in_range/pyproject.toml ================================================ [project] name = "simple-prject" version = "1.2.3" description = "Some description." authors = [ { name = "Poetry Contributors", email = "no-reply@python-poetry.org" } ] license = { text = "MIT" } readme = "README.md" keywords = ["packaging", "dependency", "poetry"] requires-python = ">=3.4" [build-system] requires = ["poetry-core<0.1"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/build_systems/core_not_in_range/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_systems/has_build_script/README.md ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_systems/has_build_script/pyproject.toml ================================================ [project] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ { name = "Poetry Contributors", email = "no-reply@python-poetry.org" } ] license = { text = "MIT" } readme = "README.md" keywords = ["packaging", "dependency", "poetry"] requires-python = ">=3.4" [tool.poetry.build] script = "build.py" generate-setup-file = true [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/build_systems/has_build_script/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_systems/multiple_build_deps/README.md ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_systems/multiple_build_deps/pyproject.toml ================================================ [project] name = "simple-prject" version = "1.2.3" description = "Some description." authors = [ { name = "Poetry Contributors", email = "no-reply@python-poetry.org" } ] license = { text = "MIT" } readme = "README.md" requires-python = ">=3.4" [build-system] requires = ["poetry-core>=1.1.0a7", "setuptools"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/build_systems/multiple_build_deps/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_systems/no_build_backend/README.md ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_systems/no_build_backend/pyproject.toml ================================================ [project] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ { name = "Poetry Contributors", email = "no-reply@python-poetry.org" } ] license = { text = "MIT" } readme = "README.md" keywords = ["packaging", "dependency", "poetry"] requires-python = ">=3.4" [build-system] requires = ["poetry-core>=1.1.0a7"] ================================================ FILE: tests/fixtures/build_systems/no_build_backend/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_systems/no_build_system/README.md ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_systems/no_build_system/pyproject.toml ================================================ [project] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ { name = "Poetry Contributors", email = "no-reply@python-poetry.org" } ] license = { text = "MIT" } readme = "README.md" keywords = ["packaging", "dependency", "poetry"] requires-python = ">=3.4" ================================================ FILE: tests/fixtures/build_systems/no_build_system/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/build_systems/no_core/README.md ================================================ My Package ========== ================================================ FILE: tests/fixtures/build_systems/no_core/pyproject.toml ================================================ [project] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ { name = "Poetry Contributors", email = "no-reply@python-poetry.org" } ] license = { text = "MIT" } readme = "README.md" keywords = ["packaging", "dependency", "poetry"] requires-python = ">=3.4" [tool.maturin] manylinux = "off" sdist-include = ["Cargo.lock", "json/**/*"] strip = "on" [build-system] build-backend = "maturin" requires = ["maturin>=0.8.1,<0.9"] ================================================ FILE: tests/fixtures/build_systems/no_core/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/complete.toml ================================================ [tool.poetry] name = "poetry" version = "0.5.0" description = "Python dependency management and packaging made easy." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" homepage = "https://python-poetry.org/" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.2" # Compatible python versions must be declared here toml = "^0.9" # Dependencies with extras requests = { version = "^2.13", extras = [ "security" ] } # Python specific dependencies with prereleases allowed pathlib2 = { version = "^2.2", python = "~2.7", allow-prereleases = true } # Git dependencies cleo = { git = "https://github.com/sdispater/cleo.git", branch = "master" } # Optional dependencies (extras) pendulum = { version = "^1.4", optional = true } [tool.poetry.extras] time = [ "pendulum" ] [tool.poetry.group.dev.dependencies] pytest = "^3.0" pytest-cov = "^2.4" [tool.poetry.scripts] my-script = 'my_package:main' [[tool.poetry.source]] name = "foo" url = "https://bar.com" ================================================ FILE: tests/fixtures/deleted_directory_dependency/pyproject.toml ================================================ [tool.poetry] name = "project-with-missing-directory-dependency" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" packages = [] [tool.poetry.dependencies] python = "*" ================================================ FILE: tests/fixtures/deleted_file_dependency/pyproject.toml ================================================ [tool.poetry] name = "project-with-missing-directory-dependency" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" packages = [] [tool.poetry.dependencies] python = "*" ================================================ FILE: tests/fixtures/directory/project_with_transitive_directory_dependencies/project_with_transitive_directory_dependencies/__init__.py ================================================ ================================================ FILE: tests/fixtures/directory/project_with_transitive_directory_dependencies/pyproject.toml ================================================ [tool.poetry] name = "project-with-transitive-directory-dependencies" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" [tool.poetry.dependencies] python = "*" project-with-extras = {path = "../../project_with_extras/"} project-with-transitive-file-dependencies = {path = "../project_with_transitive_file_dependencies/"} [tool.poetry.group.dev.dependencies] ================================================ FILE: tests/fixtures/directory/project_with_transitive_directory_dependencies/setup.py ================================================ from __future__ import annotations from distutils.core import setup packages = ["project_with_extras"] package_data = {"": ["*"]} extras_require = {"extras_a": ["pendulum>=1.4.4"], "extras_b": ["cachy>=0.2.0"]} setup_kwargs = { "name": "project-with-extras", "version": "1.2.3", "description": "This is a description", "long_description": None, "author": "Your Name", "author_email": "you@example.com", "url": None, "packages": packages, "package_data": package_data, "extras_require": extras_require, } setup(**setup_kwargs) ================================================ FILE: tests/fixtures/directory/project_with_transitive_file_dependencies/inner-directory-project/pyproject.toml ================================================ [tool.poetry] name = "inner-directory-project" version = "1.2.4" description = "This is a description" authors = ["Your Name "] license = "MIT" [tool.poetry.dependencies] python = "*" [tool.poetry.group.dev.dependencies] ================================================ FILE: tests/fixtures/directory/project_with_transitive_file_dependencies/project_with_transitive_file_dependencies/__init__.py ================================================ ================================================ FILE: tests/fixtures/directory/project_with_transitive_file_dependencies/pyproject.toml ================================================ [tool.poetry] name = "project-with-transitive-file-dependencies" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" [tool.poetry.dependencies] python = "*" demo = {path = "../../distributions/demo-0.1.0-py2.py3-none-any.whl"} inner-directory-project = {path = "./inner-directory-project"} [tool.poetry.group.dev.dependencies] ================================================ FILE: tests/fixtures/excluded_subpackage/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/excluded_subpackage/example/__init__.py ================================================ from __future__ import annotations __version__ = "0.1.0" ================================================ FILE: tests/fixtures/excluded_subpackage/example/test/__init__.py ================================================ ================================================ FILE: tests/fixtures/excluded_subpackage/example/test/excluded.py ================================================ from __future__ import annotations from tests.fixtures.excluded_subpackage.example import __version__ def test_version(): assert __version__ == "0.1.0" ================================================ FILE: tests/fixtures/excluded_subpackage/pyproject.toml ================================================ [tool.poetry] name = "example" version = "0.1.0" description = "" authors = ["Sébastien Eustace "] exclude = [ "**/test/**/*", ] [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] pytest = "^3.0" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" ================================================ FILE: tests/fixtures/extended_project/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/extended_project/build.py ================================================ from __future__ import annotations from pathlib import Path from typing import Any def build(setup_kwargs: dict[str, Any]): assert setup_kwargs["name"] == "extended-project" assert setup_kwargs["version"] == "1.2.3" dynamic_module = Path(__file__).parent / "extended_project" / "built.py" dynamic_module.write_text("# Generated by build.py") ================================================ FILE: tests/fixtures/extended_project/extended_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/extended_project/pyproject.toml ================================================ [tool.poetry] name = "extended-project" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] [tool.poetry.build] script = "build.py" generate-setup-file = true # Requirements [tool.poetry.dependencies] python = "^3.7" ================================================ FILE: tests/fixtures/extended_project_without_setup/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/extended_project_without_setup/build.py ================================================ ================================================ FILE: tests/fixtures/extended_project_without_setup/extended_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/extended_project_without_setup/pyproject.toml ================================================ [tool.poetry] name = "extended-project" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] [tool.poetry.build] script = "build.py" generate-setup-file = false # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" [build-system] requires = ["poetry-core", "cython"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/extended_with_no_setup/README.md ================================================ Module 1 ======== ================================================ FILE: tests/fixtures/extended_with_no_setup/build.py ================================================ from __future__ import annotations import os import shutil from setuptools import Distribution from setuptools import Extension from setuptools.command.build_ext import build_ext extensions = [Extension("extended.extended", ["extended/extended.c"])] def build(): distribution = Distribution({"name": "extended", "ext_modules": extensions}) distribution.package_dir = {"extended": "extended"} cmd = build_ext(distribution) cmd.ensure_finalized() cmd.run() # Copy built extensions back to the project for output in cmd.get_outputs(): relative_extension = os.path.relpath(output, cmd.build_lib) shutil.copyfile(output, relative_extension) mode = os.stat(relative_extension).st_mode mode |= (mode & 0o444) >> 2 os.chmod(relative_extension, mode) if __name__ == "__main__": build() ================================================ FILE: tests/fixtures/extended_with_no_setup/extended/__init__.py ================================================ ================================================ FILE: tests/fixtures/extended_with_no_setup/extended/extended.c ================================================ #include static PyObject *hello(PyObject *self) { return PyUnicode_FromString("Hello"); } static PyMethodDef module_methods[] = { { "hello", (PyCFunction) hello, METH_NOARGS, PyDoc_STR("Say hello.") }, {NULL} }; #if PY_MAJOR_VERSION >= 3 static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "extended", NULL, -1, module_methods, NULL, NULL, NULL, NULL, }; #endif PyMODINIT_FUNC #if PY_MAJOR_VERSION >= 3 PyInit_extended(void) #else init_extended(void) #endif { PyObject *module; #if PY_MAJOR_VERSION >= 3 module = PyModule_Create(&moduledef); #else module = Py_InitModule3("extended", module_methods, NULL); #endif if (module == NULL) #if PY_MAJOR_VERSION >= 3 return NULL; #else return; #endif #if PY_MAJOR_VERSION >= 3 return module; #endif } ================================================ FILE: tests/fixtures/extended_with_no_setup/pyproject.toml ================================================ [tool.poetry] name = "extended" version = "0.1" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.md" homepage = "https://python-poetry.org/" include = [ # C extensions must be included in the wheel distributions {path = "extended/*.so", format = "wheel"}, {path = "extended/*.pyd", format = "wheel"}, ] [tool.poetry.build] script = "build.py" generate-setup-file = false [build-system] requires = ["poetry-core>=1.5.0", "setuptools>=67.6.1"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/git/github.com/demo/demo/demo/__init__.py ================================================ from __future__ import annotations __version__ = "1.2.3" ================================================ FILE: tests/fixtures/git/github.com/demo/demo/pyproject.toml ================================================ [tool.poetry] name = "demo" version = "0.1.2" description = "Demo package" authors = ["Poetry Team "] license = "MIT" readme = "README.md" [tool.poetry.dependencies] python = "*" pendulum = ">=1.4.4" cleo = {version="*", optional = true} tomlkit = {version="*", optional = true} [tool.poetry.extras] foo = ["cleo"] bar = ["tomlkit"] [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/git/github.com/demo/namespace-package-one/namespace_package/__init__.py ================================================ from __future__ import annotations __import__("pkg_resources").declare_namespace(__name__) ================================================ FILE: tests/fixtures/git/github.com/demo/namespace-package-one/namespace_package/one/__init__.py ================================================ from __future__ import annotations name = "one" ================================================ FILE: tests/fixtures/git/github.com/demo/namespace-package-one/setup.py ================================================ from __future__ import annotations from setuptools import find_packages from setuptools import setup setup( name="namespace_package_one", version="1.0.0", description="", long_description="", author="Python Poetry", author_email="noreply@python-poetry.org", license="MIT", packages=find_packages(), namespace_packages=["namespace_package"], zip_safe=False, ) ================================================ FILE: tests/fixtures/git/github.com/demo/no-dependencies/demo/__init__.py ================================================ from __future__ import annotations __version__ = "1.2.3" ================================================ FILE: tests/fixtures/git/github.com/demo/no-dependencies/setup.py ================================================ from __future__ import annotations from setuptools import setup kwargs = dict( name="demo", license="MIT", version="0.1.2", description="Demo project.", author="Sébastien Eustace", author_email="sebastien@eustace.io", url="https://github.com/demo/demo", packages=["demo"], ) setup(**kwargs) ================================================ FILE: tests/fixtures/git/github.com/demo/no-version/demo/__init__.py ================================================ from __future__ import annotations __version__ = "1.2.3" ================================================ FILE: tests/fixtures/git/github.com/demo/no-version/setup.py ================================================ from __future__ import annotations import ast import os from setuptools import setup def read_version(): with open(os.path.join(os.path.dirname(__file__), "demo", "__init__.py")) as f: for line in f: if line.startswith("__version__ = "): return ast.literal_eval(line[len("__version__ = ") :].strip()) kwargs = dict( name="demo", license="MIT", version=read_version(), description="Demo project.", author="Sébastien Eustace", author_email="sebastien@eustace.io", url="https://github.com/demo/demo", packages=["demo"], install_requires=["pendulum>=1.4.4"], extras_require={"foo": ["cleo"]}, ) setup(**kwargs) ================================================ FILE: tests/fixtures/git/github.com/demo/non-canonical-name/demo/__init__.py ================================================ from __future__ import annotations __version__ = "1.2.3" ================================================ FILE: tests/fixtures/git/github.com/demo/non-canonical-name/setup.py ================================================ from __future__ import annotations from setuptools import setup kwargs = dict( name="Demo", license="MIT", version="0.1.2", description="Demo project.", author="Sébastien Eustace", author_email="sebastien@eustace.io", url="https://github.com/demo/demo", packages=["demo"], install_requires=["pendulum>=1.4.4"], extras_require={"foo": ["cleo"], "bar": ["tomlkit"]}, ) setup(**kwargs) ================================================ FILE: tests/fixtures/git/github.com/demo/poetry-plugin/poetry_plugin/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/demo/poetry-plugin/pyproject.toml ================================================ [tool.poetry] name = "poetry-plugin" version = "0.1.2" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" [tool.poetry.dependencies] python = "^3.6" pendulum = "^2.0" tomlkit = {version = "^0.7.0", optional = true} [tool.poetry.extras] foo = ["tomlkit"] [tool.poetry.group.dev.dependencies] ================================================ FILE: tests/fixtures/git/github.com/demo/poetry-plugin2/subdir/poetry_plugin/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/demo/poetry-plugin2/subdir/pyproject.toml ================================================ [tool.poetry] name = "poetry-plugin" version = "0.1.2" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" [tool.poetry.dependencies] python = "^3.6" pendulum = "^2.0" tomlkit = {version = "^0.7.0", optional = true} [tool.poetry.extras] foo = ["tomlkit"] [tool.poetry.group.dev.dependencies] ================================================ FILE: tests/fixtures/git/github.com/demo/prerelease/prerelease/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/demo/prerelease/pyproject.toml ================================================ [tool.poetry] name = "prerelease" version = "1.0.0.dev0" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" [tool.poetry.group.dev.dependencies] ================================================ FILE: tests/fixtures/git/github.com/demo/pyproject-demo/demo/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/demo/pyproject-demo/pyproject.toml ================================================ [tool.poetry] name = "demo" version = "0.1.2" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" pendulum = '^1.4' [tool.poetry.group.dev.dependencies] ================================================ FILE: tests/fixtures/git/github.com/demo/subdirectories/one/one/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/demo/subdirectories/one/pyproject.toml ================================================ [tool.poetry] name = "one" version = "1.0.0" description = "Some description." authors = [] license = "MIT" [tool.poetry.dependencies] python = "^3.7" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/git/github.com/demo/subdirectories/one-copy/one/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/demo/subdirectories/one-copy/pyproject.toml ================================================ [tool.poetry] name = "one" version = "1.0.0" description = "Some description." authors = [] license = "MIT" [tool.poetry.dependencies] python = "^3.7" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/git/github.com/demo/subdirectories/two/pyproject.toml ================================================ [tool.poetry] name = "two" version = "2.0.0" description = "Some description." authors = [] license = "MIT" [tool.poetry.dependencies] python = "~2.7 || ^3.4" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/git/github.com/demo/subdirectories/two/two/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/forked_demo/subdirectories/one/one/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/forked_demo/subdirectories/one/pyproject.toml ================================================ [tool.poetry] name = "one" version = "1.0.0" description = "Some description." authors = [] license = "MIT" [tool.poetry.dependencies] python = "^3.7" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/one/__init__.py ================================================ ================================================ FILE: tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/pyproject.toml ================================================ [tool.poetry] name = "one" version = "1.0.0" description = "Some description." authors = [] license = "MIT" [tool.poetry.dependencies] python = "^3.7" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/git/github.com/forked_demo/subdirectories/two/pyproject.toml ================================================ [tool.poetry] name = "two" version = "2.0.0" description = "Some description." authors = [] license = "MIT" [tool.poetry.dependencies] python = "~2.7 || ^3.4" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/git/github.com/forked_demo/subdirectories/two/two/__init__.py ================================================ ================================================ FILE: tests/fixtures/incompatible_lock/pyproject.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Poetry Developer "] [tool.poetry.dependencies] python = "^3.8" sampleproject = ">=1.3.1" [tool.poetry.group.dev.dependencies] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/inspection/demo/pyproject.toml ================================================ [tool.poetry] name = "demo" version = "0.1.0" description = "" authors = ["Sébastien Eustace "] [tool.poetry.dependencies] python = "~2.7 || ^3.4" pendulum = ">=1.4.4" cleo = {version = "*", optional = true} tomlkit = {version = "*", optional = true} [tool.poetry.extras] foo = ["cleo"] bar = ["tomlkit"] [tool.poetry.group.dev.dependencies] pytest = "^3.0" ================================================ FILE: tests/fixtures/inspection/demo_no_setup_pkg_info_no_deps/PKG-INFO ================================================ Metadata-Version: 1.0 Name: demo Version: 0.1.0 Summary: Demo project. Home-page: https://github.com/demo/demo Author: Sébastien Eustace Author-email: sebastien@eustace.io License: MIT Description: UNKNOWN Platform: UNKNOWN ================================================ FILE: tests/fixtures/inspection/demo_no_setup_pkg_info_no_deps/pyproject.toml ================================================ # this was copied over and modified from orjson project's pyproject.toml # https://github.com/ijl/orjson/blob/master/pyproject.toml [project] name = "demo" repository = "https://github.com/demo/demo" [build-system] build-backend = "maturin" requires = ["maturin>=0.8.1,<0.9"] [tool.maturin] manylinux = "off" sdist-include = ["Cargo.lock", "json/**/*"] strip = "on" [tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38'] include = '\.pyi?$' ================================================ FILE: tests/fixtures/inspection/demo_no_setup_pkg_info_no_deps_dynamic/PKG-INFO ================================================ Metadata-Version: 2.2 Name: demo Version: 0.1.0 Summary: Demo project. Home-page: https://github.com/demo/demo Author: Sébastien Eustace Author-email: sebastien@eustace.io License: MIT Description: UNKNOWN Platform: UNKNOWN Dynamic: Requires-Dist ================================================ FILE: tests/fixtures/inspection/demo_no_setup_pkg_info_no_deps_dynamic/pyproject.toml ================================================ # this was copied over and modified from orjson project's pyproject.toml # https://github.com/ijl/orjson/blob/master/pyproject.toml [project] name = "demo" repository = "https://github.com/demo/demo" [build-system] build-backend = "maturin" requires = ["maturin>=0.8.1,<0.9"] [tool.maturin] manylinux = "off" sdist-include = ["Cargo.lock", "json/**/*"] strip = "on" [tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38'] include = '\.pyi?$' ================================================ FILE: tests/fixtures/inspection/demo_no_setup_pkg_info_no_deps_for_sure/PKG-INFO ================================================ Metadata-Version: 2.3 Name: demo Version: 0.1.0 Summary: Demo project. Home-page: https://github.com/demo/demo Author: Sébastien Eustace Author-email: sebastien@eustace.io License: MIT Description: UNKNOWN Platform: UNKNOWN ================================================ FILE: tests/fixtures/inspection/demo_no_setup_pkg_info_no_deps_for_sure/pyproject.toml ================================================ # this was copied over and modified from orjson project's pyproject.toml # https://github.com/ijl/orjson/blob/master/pyproject.toml [project] name = "demo" repository = "https://github.com/demo/demo" [build-system] build-backend = "maturin" requires = ["maturin>=0.8.1,<0.9"] [tool.maturin] manylinux = "off" sdist-include = ["Cargo.lock", "json/**/*"] strip = "on" [tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38'] include = '\.pyi?$' ================================================ FILE: tests/fixtures/inspection/demo_only_requires_txt.egg-info/PKG-INFO ================================================ Metadata-Version: 1.0 Name: demo Version: 0.1.0 Summary: Demo project. Home-page: https://github.com/demo/demo Author: Sébastien Eustace Author-email: sebastien@eustace.io License: MIT Description: UNKNOWN Platform: UNKNOWN ================================================ FILE: tests/fixtures/inspection/demo_only_requires_txt.egg-info/requires.txt ================================================ cleo; extra == "foo" pendulum (>=1.4.4) tomlkit; extra == "bar" ================================================ FILE: tests/fixtures/inspection/demo_poetry_package/pyproject.toml ================================================ [tool.poetry] name = "demo-poetry" version = "0.1.0" description = "" authors = ["John Doe "] [tool.poetry.dependencies] python = "^3.10" pendulum = "*" [tool.poetry.group.dev.dependencies] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/inspection/demo_with_obsolete_egg_info/demo-0.1.0.egg-info/PKG-INFO ================================================ Metadata-Version: 1.0 Name: demo Version: 0.1.0 Summary: Demo project. Home-page: https://github.com/demo/demo Author: Sébastien Eustace Author-email: sebastien@eustace.io License: MIT Description: UNKNOWN Platform: UNKNOWN ================================================ FILE: tests/fixtures/inspection/demo_with_obsolete_egg_info/demo-0.1.0.egg-info/requires.txt ================================================ cleo; extra == "foo" pendulum (>=1.0.0) tomlkit; extra == "bar" ================================================ FILE: tests/fixtures/inspection/demo_with_obsolete_egg_info/pyproject.toml ================================================ [tool.poetry] name = "demo" version = "0.1.0" description = "" authors = ["Sébastien Eustace "] [tool.poetry.dependencies] python = "~2.7 || ^3.4" pendulum = ">=1.4.4" cleo = {version = "*", optional = true} tomlkit = {version = "*", optional = true} [tool.poetry.extras] foo = ["cleo"] bar = ["tomlkit"] [tool.poetry.group.dev.dependencies] pytest = "^3.0" ================================================ FILE: tests/fixtures/invalid_lock/pyproject.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Poetry Developer "] [tool.poetry.dependencies] python = "^3.8" sampleproject = ">=1.3.1" [tool.poetry.group.dev.dependencies] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/invalid_pyproject/pyproject.toml ================================================ [project] name = "invalid" version = "1.0.0" license = "INVALID" classifiers = [ "Environment :: Console", "Intended Audience :: Clowns", "Natural Language :: Ukranian", "Topic :: Communications :: Chat :: AOL Instant Messenger", ] dynamic = [ "readme", "dependencies", "requires-python" ] [tool.poetry] readme = "never/exists.md" [tool.poetry.dependencies] python = "*" pendulum = {"version" = "^2.0.5", allows-prereleases = true} invalid_dep = "1.0" invalid_source = { "version" = "*", source = "not-exists" } invalid_source_multi = [ { "version" = "*", platform = "linux", source = "exists" }, { "version" = "*", platform = "win32", source = "not-exists2" }, ] [[tool.poetry.source]] name = "exists" priority = "explicit" url = "https://example.com" ================================================ FILE: tests/fixtures/invalid_pyproject_dep_name/pyproject.toml ================================================ [project] name = "invalid" version = "1.0.0" dynamic = ["dependencies"] [tool.poetry.dependencies] invalid = "1.0" ================================================ FILE: tests/fixtures/missing_directory_dependency/pyproject.toml ================================================ [tool.poetry] name = "project-with-missing-directory-dependency" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" packages = [] [tool.poetry.dependencies] python = "*" [tool.poetry.group.dev.dependencies] missing = { path = "./missing" } ================================================ FILE: tests/fixtures/missing_extra_directory_dependency/pyproject.toml ================================================ [tool.poetry] name = "project-with-missing-extra-directory-dependency" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" packages = [] [tool.poetry.dependencies] python = "*" missing = { path = "./missing", optional = true } [tool.poetry.extras] notinstallable = ["missing"] ================================================ FILE: tests/fixtures/missing_file_dependency/pyproject.toml ================================================ [tool.poetry] name = "project-with-missing-directory-dependency" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" packages = [] [tool.poetry.dependencies] python = "*" [tool.poetry.group.dev.dependencies] missing = { file = "missing-0.1.0-py2.py3-none-any.whl" } ================================================ FILE: tests/fixtures/nameless_pyproject/pyproject.toml ================================================ [tool.poetry] version = "0.1.0" description = "" authors = ["Foo "] readme = "README.md" [tool.poetry.dependencies] python = "^3.10" ================================================ FILE: tests/fixtures/no_name_project/README.rst ================================================ No name project =============== ================================================ FILE: tests/fixtures/no_name_project/pyproject.toml ================================================ [tool.poetry] package-mode = false version = "1.2.3" description = "This project has no name" authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.6" [tool.poetry.group.dev.dependencies] pytest = "~3.4" ================================================ FILE: tests/fixtures/non_package_mode/pyproject.toml ================================================ [tool.poetry] package-mode = false [tool.poetry.dependencies] python = "^3.8" cleo = "^0.6" pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } ================================================ FILE: tests/fixtures/old_lock/pyproject.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Poetry Developer "] [tool.poetry.dependencies] python = "^3.8" sampleproject = ">=1.3.1" [tool.poetry.group.dev.dependencies] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/old_lock_path_dependency/pyproject.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Poetry Developer "] [tool.poetry.dependencies] python = "^3.8" quix = { path = "./quix", develop = true} [tool.poetry.group.dev.dependencies] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/old_lock_path_dependency/quix/pyproject.toml ================================================ [tool.poetry] name = "quix" version = "1.2.3" description = "Some description." authors = ["Poetry Maintainer "] license = "MIT" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" sampleproject = ">=1.3.1" ================================================ FILE: tests/fixtures/outdated_lock/pyproject.toml ================================================ [project] name = "foobar" version = "0.1.0" requires-python = ">=3.8,<4.0" dependencies = [ "docker>=4.3.1", ] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/private_pyproject/pyproject.toml ================================================ [project] name = "private" version = "0.1.0" requires-python = ">=3.7,<4.0" classifiers = [ "Private :: Do Not Upload", ] [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: my-application-plugin Version: 1.0 Summary: description Requires-Python: >=3.8,<4.0 Requires-Dist: poetry (>=1.8.0,<3.0.0) Requires-Dist: some-lib (>=1.7.0,<3.0.0) ================================================ FILE: tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/entry_points.txt ================================================ [poetry.application.plugin] my-command=my_application_plugin.plugins:MyApplicationPlugin ================================================ FILE: tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: my-application-plugin Version: 2.0 Summary: description Requires-Python: >=3.8,<4.0 Requires-Dist: poetry (>=1.8.0,<3.0.0) Requires-Dist: some-lib (>=1.7.0,<3.0.0) ================================================ FILE: tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/entry_points.txt ================================================ [poetry.application.plugin] my-command=my_application_plugin.plugins:MyApplicationPlugin ================================================ FILE: tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: my-other-plugin Version: 1.0 Summary: description Requires-Python: >=3.8,<4.0 Requires-Dist: poetry (>=1.8.0,<3.0.0) Requires-Dist: some-lib (>=1.7.0,<3.0.0) ================================================ FILE: tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/entry_points.txt ================================================ [poetry.plugin] other-plugin=my_application_plugin.plugins:MyOtherPlugin ================================================ FILE: tests/fixtures/project_plugins/pyproject.toml ================================================ [tool.poetry] package-mode = false [tool.poetry.requires-plugins] my-application-plugin = ">=2.0" my-other-plugin = ">=1.0" ================================================ FILE: tests/fixtures/project_plugins/some_lib-1.0.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: some-lib Version: 1.0 Summary: description Requires-Python: >=3.8,<4.0 ================================================ FILE: tests/fixtures/project_plugins/some_lib-2.0.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: some-lib Version: 2.0 Summary: description Requires-Python: >=3.8,<4.0 ================================================ FILE: tests/fixtures/project_with_extras/project_with_extras/__init__.py ================================================ ================================================ FILE: tests/fixtures/project_with_extras/pyproject.toml ================================================ [tool.poetry] name = "project-with-extras" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" [tool.poetry.dependencies] python = "*" pendulum = { version = ">=1.4.4", optional = true } cachy = { version = ">=0.2.0", optional = true } [tool.poetry.extras] extras_a = [ "pendulum" ] extras_b = [ "cachy" ] ================================================ FILE: tests/fixtures/project_with_git_dev_dependency/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" cachy = "^0.1.0" pendulum = "^2.0.0" [tool.poetry.group.dev.dependencies] pytest = "~3.4" demo = { git = "https://github.com/demo/demo.git", rev = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24" } [tool.poetry.scripts] my-script = "my_package:main" [tool.poetry.plugins."blogtool.parsers"] ".rst" = "some_module::SomeClass" ================================================ FILE: tests/fixtures/project_with_local_dependencies/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" # File dependency demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } # Dir dependency with setup.py project-with-setup = { path = "../project_with_setup/" } [tool.poetry.scripts] my-script = "my_package:main" [tool.poetry.plugins."blogtool.parsers"] ".rst" = "some_module::SomeClass" ================================================ FILE: tests/fixtures/project_with_multi_constraints_dependency/project/__init__.py ================================================ ================================================ FILE: tests/fixtures/project_with_multi_constraints_dependency/pyproject.toml ================================================ [tool.poetry] name = "project-with-multi-constraints-dependency" version = "1.2.3" description = "This is a description" authors = ["Your Name "] license = "MIT" packages = [ {include = "project"} ] [tool.poetry.dependencies] python = "*" pendulum = [ { version = "^1.5", python = "<3.4" }, { version = "^2.0", python = "^3.4" } ] [tool.poetry.group.dev.dependencies] ================================================ FILE: tests/fixtures/project_with_nested_local/bar/pyproject.toml ================================================ [tool.poetry] name = "bar" version = "1.2.3" description = "Some description." authors = ["Poetry Maintainer "] license = "MIT" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" quix = { path = "../quix", develop = true } ================================================ FILE: tests/fixtures/project_with_nested_local/foo/pyproject.toml ================================================ [tool.poetry] name = "foo" version = "1.2.3" description = "Some description." authors = ["Poetry Maintainer "] license = "MIT" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" bar = { path = "../bar", develop = true } ================================================ FILE: tests/fixtures/project_with_nested_local/pyproject.toml ================================================ [tool.poetry] name = "project-with-nested-local" version = "1.2.3" description = "Some description." authors = ["Poetry Maintainer "] license = "MIT" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" foo = { path = "./foo", develop = true } bar = { path = "./bar", develop = true } ================================================ FILE: tests/fixtures/project_with_nested_local/quix/pyproject.toml ================================================ [tool.poetry] name = "quix" version = "1.2.3" description = "Some description." authors = ["Poetry Maintainer "] license = "MIT" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" ================================================ FILE: tests/fixtures/project_with_setup/my_package/__init__.py ================================================ ================================================ FILE: tests/fixtures/project_with_setup/setup.py ================================================ from __future__ import annotations from setuptools import setup kwargs = dict( name="project-with-setup", license="MIT", version="0.1.2", description="Demo project.", author="Sébastien Eustace", author_email="sebastien@eustace.io", url="https://github.com/demo/demo", packages=["my_package"], install_requires=["pendulum>=1.4.4", "cachy[msgpack]>=0.2.0"], ) setup(**kwargs) ================================================ FILE: tests/fixtures/project_with_setup_calls_script/my_package/__init__.py ================================================ ================================================ FILE: tests/fixtures/project_with_setup_calls_script/pyproject.toml ================================================ [build-system] requires = ["setuptools", ""] build-backend = "setuptools.build_meta:__legacy__" ================================================ FILE: tests/fixtures/project_with_setup_calls_script/setup.py ================================================ from __future__ import annotations import subprocess from setuptools import setup if subprocess.call(["exit-code"]) != 42: raise RuntimeError("Wrong exit code.") kwargs = dict( name="project-with-setup-calls-script", license="MIT", version="0.1.2", description="Demo project.", author="Sébastien Eustace", author_email="sebastien@eustace.io", url="https://github.com/demo/demo", packages=["my_package"], install_requires=["pendulum>=1.4.4", "cachy[msgpack]>=0.2.0"], ) setup(**kwargs) ================================================ FILE: tests/fixtures/pypi_reference/pyproject.toml ================================================ [project] name = "foobar" version = "0.1.0" description = "" authors = [ { name = "Poetry Developer", email = "" } ] dynamic = ["dependencies", "requires-python"] [tool.poetry.dependencies] python = "^3.8" docker = { version = ">=4.3.1", source = "PyPI" } [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/sample_project/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/sample_project/pyproject.toml ================================================ [tool.poetry] name = "sample-project" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.6" cleo = "^0.6" pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } requests = { version = "^2.18", optional = true, extras=[ "security" ] } pathlib2 = { version = "^2.2", python = "~2.7" } orator = { version = "^0.9", optional = true } # File dependency demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } # Dir dependency with setup.py my-package = { path = "../project_with_setup/" } # Dir dependency with pyproject.toml simple-project = { path = "../simple_project/" } # Dependency with markers functools32 = { version = "^3.2.3", markers = "python_version ~= '2.7' and sys_platform == 'win32' or python_version in '3.4 3.5'" } [tool.poetry.extras] db = [ "orator" ] [tool.poetry.group.dev.dependencies] pytest = "~3.4" [tool.poetry.scripts] my-script = "sample_project:main" [tool.poetry.plugins."blogtool.parsers"] ".rst" = "some_module::SomeClass" ================================================ FILE: tests/fixtures/scripts/README.md ================================================ ================================================ FILE: tests/fixtures/scripts/pyproject.toml ================================================ [tool.poetry] name = "scripts" version = "0.1.0" description = "" authors = ["Your Name "] readme = "README.md" [tool.poetry.dependencies] python = "^3.7" [tool.poetry.scripts] check-argv0 = "scripts.check_argv0:main" exit-code = "scripts.exit_code:main" return-code = "scripts.return_code:main" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/scripts/scripts/__init__.py ================================================ ================================================ FILE: tests/fixtures/scripts/scripts/check_argv0.py ================================================ from __future__ import annotations import sys from pathlib import Path def main() -> int: path = Path(sys.argv[0]) if sys.argv[1] == "absolute": if not path.is_absolute(): raise RuntimeError(f"sys.argv[0] is not an absolute path: {path}") if not path.exists(): raise RuntimeError(f"sys.argv[0] does not exist: {path}") else: if path.is_absolute(): raise RuntimeError(f"sys.argv[0] is an absolute path: {path}") return 0 if __name__ == "__main__": raise sys.exit(main()) ================================================ FILE: tests/fixtures/scripts/scripts/exit_code.py ================================================ from __future__ import annotations def main() -> None: raise SystemExit(42) if __name__ == "__main__": main() ================================================ FILE: tests/fixtures/scripts/scripts/return_code.py ================================================ from __future__ import annotations def main() -> int: return 42 if __name__ == "__main__": raise SystemExit(main()) ================================================ FILE: tests/fixtures/self_version_not_ok/pyproject.toml ================================================ [tool.poetry] package-mode = false requires-poetry = "<1.2" [tool.poetry.dependencies] python = "^3.8" ================================================ FILE: tests/fixtures/self_version_not_ok_invalid_config/pyproject.toml ================================================ [tool.poetry] package-mode = false requires-poetry = "<1.2" invalid_option = 42 [tool.poetry.dependencies] python = "^3.8" ================================================ FILE: tests/fixtures/self_version_ok/pyproject.toml ================================================ [tool.poetry] package-mode = false requires-poetry = ">=1.2" [tool.poetry.dependencies] python = "^3.8" ================================================ FILE: tests/fixtures/simple_project/LICENSE ================================================ license ... ================================================ FILE: tests/fixtures/simple_project/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/simple_project/pyproject.toml ================================================ [project] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ { name = "Sébastien Eustace", email = "sebastien@eustace.io" } ] license = "MIT" readme = "README.rst" keywords = ["packaging", "dependency", "poetry"] dynamic = [ "classifiers", "dependencies", "requires-python" ] [project.urls] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" [project.scripts] foo = "foo:bar" baz = "bar:baz.boom.bim" fox = "fuz.foo:bar.baz" [tool.poetry] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/simple_project/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/simple_project_legacy/LICENSE ================================================ license ... ================================================ FILE: tests/fixtures/simple_project_legacy/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/simple_project_legacy/pyproject.toml ================================================ [tool.poetry] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = ["README.rst"] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" [tool.poetry.scripts] foo = "foo:bar" baz = "bar:baz.boom.bim" fox = "fuz.foo:bar.baz" [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/simple_project_legacy/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/up_to_date_lock/pyproject.toml ================================================ [project] name = "foobar" version = "0.1.0" requires-python = ">=3.8,<4.0" dependencies = [ "docker>=4.3.1", ] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/up_to_date_lock_non_package/pyproject.toml ================================================ [tool.poetry] package-mode = false [tool.poetry.dependencies] python = "^3.8" docker = ">=4.3.1" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/with-include/LICENSE ================================================ Copyright (c) 2018 Sébastien Eustace Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: tests/fixtures/with-include/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/with-include/extra_dir/README.md ================================================ ================================================ FILE: tests/fixtures/with-include/extra_dir/__init__.py ================================================ ================================================ FILE: tests/fixtures/with-include/extra_dir/sub_pkg/__init__.py ================================================ ================================================ FILE: tests/fixtures/with-include/extra_dir/sub_pkg/vcs_excluded.txt ================================================ ================================================ FILE: tests/fixtures/with-include/extra_dir/vcs_excluded.txt ================================================ ================================================ FILE: tests/fixtures/with-include/for_wheel_only/__init__.py ================================================ ================================================ FILE: tests/fixtures/with-include/my_module.py ================================================ ================================================ FILE: tests/fixtures/with-include/notes.txt ================================================ ================================================ FILE: tests/fixtures/with-include/package_with_include/__init__.py ================================================ from __future__ import annotations __version__ = "1.2.3" ================================================ FILE: tests/fixtures/with-include/pyproject.toml ================================================ [tool.poetry] name = "with-include" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" homepage = "https://python-poetry.org/" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] packages = [ { include = "extra_dir/**/*.py" }, { include = "extra_dir/**/*.py" }, { include = "my_module.py" }, { include = "package_with_include" }, { include = "tests", format = "sdist" }, { include = "for_wheel_only", format = ["wheel"] }, { include = "src_package", from = "src"}, ] include = [ "extra_dir/vcs_excluded.txt", "notes.txt" ] # Requirements [tool.poetry.dependencies] python = "^3.6" cleo = "^0.6" cachy = { version = "^0.2.0", extras = ["msgpack"] } pendulum = { version = "^1.4", optional = true } [tool.poetry.group.dev.dependencies] pytest = "~3.4" [tool.poetry.extras] time = ["pendulum"] [tool.poetry.scripts] my-script = "my_package:main" my-2nd-script = "my_package:main2" ================================================ FILE: tests/fixtures/with-include/src/src_package/__init__.py ================================================ ================================================ FILE: tests/fixtures/with-include/tests/__init__.py ================================================ ================================================ FILE: tests/fixtures/with_conditional_path_deps/demo_one/pyproject.toml ================================================ [tool.poetry] name = "demo" version = "1.2.3" description = "Some description." authors = [] license = "MIT" [tool.poetry.dependencies] python = "^3.7" ================================================ FILE: tests/fixtures/with_conditional_path_deps/demo_two/pyproject.toml ================================================ [tool.poetry] name = "demo" version = "1.2.3" description = "Some description." authors = [] license = "MIT" [tool.poetry.dependencies] python = "^3.7" ================================================ FILE: tests/fixtures/with_conditional_path_deps/pyproject.toml ================================================ [tool.poetry] name = "sample" version = "1.0.0" description = "Sample Project" authors = [] license = "MIT" [tool.poetry.dependencies] python = "^3.7" demo = [ { path = "demo_one", platform = "linux" }, { path = "demo_two", platform = "win32" }, ] ================================================ FILE: tests/fixtures/with_explicit_pypi_and_other/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" [[tool.poetry.source]] name = "PyPI" priority = "explicit" ================================================ FILE: tests/fixtures/with_explicit_pypi_and_other_explicit/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "explicit" url = "https://explicit.com/simple/" priority = "explicit" [[tool.poetry.source]] name = "PyPI" priority = "explicit" ================================================ FILE: tests/fixtures/with_explicit_pypi_no_other/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "PyPI" priority = "explicit" ================================================ FILE: tests/fixtures/with_explicit_source/pyproject.toml ================================================ [tool.poetry] name = "with-explicit-source" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "explicit" url = "https://explicit.com/simple/" priority = "explicit" ================================================ FILE: tests/fixtures/with_local_config/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/with_local_config/poetry.toml ================================================ [virtualenvs] in-project = false create = false ================================================ FILE: tests/fixtures/with_local_config/pyproject.toml ================================================ [tool.poetry] name = "local-config" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.6" cleo = "^0.6" pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } requests = { version = "^2.18", optional = true, extras=[ "security" ] } pathlib2 = { version = "^2.2", python = "~2.7" } orator = { version = "^0.9", optional = true } # File dependency demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } # Dir dependency with setup.py my-package = { path = "../project_with_setup/" } # Dir dependency with pyproject.toml simple-project = { path = "../simple_project/" } [tool.poetry.extras] db = [ "orator" ] [tool.poetry.group.dev.dependencies] pytest = "~3.4" [tool.poetry.scripts] my-script = "local_config:main" [tool.poetry.plugins."blogtool.parsers"] ".rst" = "some_module::SomeClass" ================================================ FILE: tests/fixtures/with_multiple_dist_dir/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/with_multiple_dist_dir/pyproject.toml ================================================ [tool.poetry] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = ["README.rst"] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" [tool.poetry.scripts] foo = "foo:bar" baz = "bar:baz.boom.bim" fox = "fuz.foo:bar.baz" [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/with_multiple_dist_dir/simple_project/__init__.py ================================================ ================================================ FILE: tests/fixtures/with_multiple_readme_files/README-1.rst ================================================ Single Python ============= ================================================ FILE: tests/fixtures/with_multiple_readme_files/README-2.rst ================================================ Changelog ========= ================================================ FILE: tests/fixtures/with_multiple_readme_files/my_package/__init__.py ================================================ """Example module""" from __future__ import annotations __version__ = "0.1" ================================================ FILE: tests/fixtures/with_multiple_readme_files/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "0.1" description = "Some description." authors = [ "Your Name " ] license = "MIT" readme = [ "README-1.rst", "README-2.rst" ] homepage = "https://python-poetry.org" [tool.poetry.dependencies] python = "^3.7" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/with_multiple_sources/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" priority = "supplemental" [[tool.poetry.source]] name = "bar" url = "https://bar.baz/simple/" ================================================ FILE: tests/fixtures/with_multiple_sources_pypi/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" priority = "supplemental" [[tool.poetry.source]] name = "bar" url = "https://bar.baz/simple/" [[tool.poetry.source]] name = "PyPI" [[tool.poetry.source]] name = "baz" url = "https://baz.bar/simple/" ================================================ FILE: tests/fixtures/with_multiple_supplemental_sources/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" priority = "supplemental" [[tool.poetry.source]] name = "bar" url = "https://bar.baz/simple/" priority = "supplemental" ================================================ FILE: tests/fixtures/with_path_dependency/bazz/pyproject.toml ================================================ [tool.poetry] name = "bazz" version = "1" description = "Demo package" authors = ["Poetry Team "] license = "MIT" readme = "README.md" packages = [ { include = "demo", from = "src" } ] [tool.poetry.dependencies] python = "*" requests = "~=2.25.1" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/with_path_dependency/pyproject.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = [] readme = "README.md" packages = [{include = "foobar"}] [tool.poetry.dependencies] python = "^3.9" bazz = { path = "./bazz", develop = true } [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/fixtures/with_primary_source_explicit/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" priority = "primary" ================================================ FILE: tests/fixtures/with_primary_source_implicit/pyproject.toml ================================================ [tool.poetry] name = "my-package" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" ================================================ FILE: tests/fixtures/with_source/README.rst ================================================ My Package ========== ================================================ FILE: tests/fixtures/with_source/pyproject.toml ================================================ [tool.poetry] name = "with-source" version = "1.2.3" description = "Some description." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.rst" homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] # Requirements [tool.poetry.dependencies] python = "^3.6" cleo = "^0.6" pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } requests = { version = "^2.18", optional = true, extras=[ "security" ] } pathlib2 = { version = "^2.2", python = "~3.6" } orator = { version = "^0.9", optional = true } # File dependency demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } # Dir dependency with setup.py my-package = { path = "../project_with_setup/" } # Dir dependency with pyproject.toml simple-project = { path = "../simple_project/" } [tool.poetry.extras] db = [ "orator" ] [tool.poetry.group.dev.dependencies] pytest = "~3.4" [tool.poetry.scripts] my-script = "with_default_source:main" [tool.poetry.plugins."blogtool.parsers"] ".rst" = "some_module::SomeClass" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" ================================================ FILE: tests/fixtures/with_supplemental_source/pyproject.toml ================================================ [tool.poetry] name = "with-explicit-source" version = "1.2.3" description = "Some description." authors = [ "Your Name " ] license = "MIT" # Requirements [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] [[tool.poetry.source]] name = "supplemental" url = "https://supplemental.com/simple/" priority = "supplemental" ================================================ FILE: tests/helpers.py ================================================ from __future__ import annotations import contextlib import os import re import shutil from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING import keyring from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link from poetry.core.vcs.git import ParsedUrl from poetry.config.config import Config from poetry.console.application import Application from poetry.factory import Factory from poetry.installation.executor import Executor from poetry.packages import Locker from poetry.repositories import Repository from poetry.repositories.exceptions import PackageNotFoundError from poetry.utils.password_manager import PoetryKeyring if TYPE_CHECKING: from collections.abc import Iterator from collections.abc import Mapping from typing import Any import responses from keyring.backend import KeyringBackend from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency from pytest_mock import MockerFixture from requests import PreparedRequest from tomlkit.toml_document import TOMLDocument from poetry.installation.operations.operation import Operation from poetry.poetry import Poetry from tests.types import HttpResponse FIXTURE_PATH = Path(__file__).parent / "fixtures" FIXTURE_PATH_INSTALLATION = Path(__file__).parent / "installation" / "fixtures" FIXTURE_PATH_DISTRIBUTIONS = FIXTURE_PATH / "distributions" FIXTURE_PATH_REPOSITORIES = Path(__file__).parent / "repositories" / "fixtures" FIXTURE_PATH_REPOSITORIES_LEGACY = FIXTURE_PATH_REPOSITORIES / "legacy" FIXTURE_PATH_REPOSITORIES_PYPI = FIXTURE_PATH_REPOSITORIES / "pypi.org" # Used as a mock for latest git revision. MOCK_DEFAULT_GIT_REVISION = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24" def get_package( name: str, version: str | Version, yanked: str | bool = False ) -> Package: return Package(name, version, yanked=yanked) def get_dependency( name: str, constraint: str | dict[str, Any] | None = None, groups: list[str] | None = None, optional: bool = False, allows_prereleases: bool = False, ) -> Dependency: if constraint is None: constraint = "*" if isinstance(constraint, str): constraint = {"version": constraint} constraint["optional"] = optional constraint["allow-prereleases"] = allows_prereleases return Factory.create_dependency(name, constraint or "*", groups=groups) def copy_path(source: Path, dest: Path) -> None: if dest.is_dir(): shutil.rmtree(dest) else: dest.unlink(missing_ok=True) if source.is_dir(): shutil.copytree(source, dest) else: shutil.copyfile(source, dest) class MockDulwichRepo: def __init__(self, root: Path | str, **__: Any) -> None: self.path = str(root) def head(self) -> bytes: return MOCK_DEFAULT_GIT_REVISION.encode() def mock_clone( url: str, *_: Any, source_root: Path | None = None, **__: Any, ) -> MockDulwichRepo: # Checking source to determine which folder we need to copy parsed = ParsedUrl.parse(url) assert parsed.pathname is not None path = re.sub(r"(.git)?$", "", parsed.pathname.lstrip("/")) assert parsed.resource is not None folder = FIXTURE_PATH / "git" / parsed.resource / path assert folder.is_dir() if not source_root: source_root = Path(Config.create().get("cache-dir")) / "src" assert parsed.name is not None dest = source_root / parsed.name dest.mkdir(parents=True, exist_ok=True) copy_path(folder, dest) return MockDulwichRepo(dest) class TestExecutor(Executor): # class name begins 'Test': tell pytest that it does not contain testcases. __test__ = False def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._installs: list[Package] = [] self._updates: list[Package] = [] self._uninstalls: list[Package] = [] @property def installations(self) -> list[Package]: return self._installs @property def updates(self) -> list[Package]: return self._updates @property def removals(self) -> list[Package]: return self._uninstalls def _do_execute_operation(self, operation: Operation) -> int: rc = super()._do_execute_operation(operation) if not operation.skipped: getattr(self, f"_{operation.job_type}s").append(operation.package) return rc def _execute_install(self, operation: Operation) -> int: return 0 def _execute_update(self, operation: Operation) -> int: return 0 def _execute_remove(self, operation: Operation) -> int: return 0 class PoetryTestApplication(Application): def __init__(self, poetry: Poetry) -> None: super().__init__() self._poetry = poetry def reset_poetry(self) -> None: assert self._poetry is not None poetry = self._poetry self._poetry = Factory().create_poetry(self._poetry.file.path.parent) self._poetry.set_pool(poetry.pool) self._poetry.set_config(poetry.config) self._poetry.set_locker( TestLocker(poetry.locker.lock, self._poetry.pyproject.data) ) class TestLocker(Locker): # class name begins 'Test': tell pytest that it does not contain testcases. __test__ = False def __init__(self, lock: Path, pyproject_data: dict[str, Any]) -> None: super().__init__(lock, pyproject_data) self._locked = False self._write = False def write(self, write: bool = True) -> None: self._write = write def is_locked(self) -> bool: return self._locked def locked(self, is_locked: bool = True) -> TestLocker: self._locked = is_locked return self def mock_lock_data(self, data: dict[str, Any]) -> None: self.locked() self._lock_data = data def is_fresh(self) -> bool: return True def _write_lock_data(self, data: TOMLDocument) -> None: if self._write: super()._write_lock_data(data) self._locked = True return self._lock_data = data class TestRepository(Repository): def find_packages(self, dependency: Dependency) -> list[Package]: packages = super().find_packages(dependency) if len(packages) == 0: raise PackageNotFoundError(f"Package [{dependency.name}] not found.") return packages def find_links_for_package(self, package: Package) -> list[Link]: return [ Link( f"https://foo.bar/files/{package.name.replace('-', '_')}" f"-{package.version.to_string()}-py2.py3-none-any.whl" ) ] @contextlib.contextmanager def isolated_environment( environ: dict[str, Any] | None = None, clear: bool = False ) -> Iterator[None]: original_environ = dict(os.environ) if clear: os.environ.clear() if environ: os.environ.update(environ) yield os.environ.clear() os.environ.update(original_environ) def make_entry_point_from_plugin( name: str, cls: type[Any], dist: metadata.Distribution | None = None ) -> metadata.EntryPoint: group: str | None = getattr(cls, "group", None) ep: metadata.EntryPoint = metadata.EntryPoint( name=name, group=group, # type: ignore[arg-type] value=f"{cls.__module__}:{cls.__name__}", ) if dist: ep = ep._for(dist) # type: ignore[attr-defined,no-untyped-call] return ep return ep def mock_metadata_entry_points( mocker: MockerFixture, cls: type[Any], name: str = "my-plugin", dist: metadata.Distribution | None = None, ) -> None: def patched_entry_points(*args: Any, **kwargs: Any) -> list[metadata.EntryPoint]: if "group" in kwargs and kwargs["group"] != getattr(cls, "group", None): return [] return [make_entry_point_from_plugin(name, cls, dist)] mocker.patch.object( metadata, "entry_points", side_effect=patched_entry_points, ) def flatten_dict(obj: Mapping[str, Any], delimiter: str = ".") -> Mapping[str, Any]: """ Flatten a nested dict. A flatdict replacement. :param obj: A nested dict to be flattened :delimiter str: A delimiter used in the key path :return: Flattened dict """ def recurse_keys(obj: Mapping[str, Any]) -> Iterator[tuple[list[str], Any]]: """ A recursive generator to yield key paths and their values :param obj: A nested dict to be flattened :return: dict """ if isinstance(obj, dict): for key in obj: for leaf in recurse_keys(obj[key]): leaf_path, leaf_value = leaf leaf_path.insert(0, key) yield (leaf_path, leaf_value) else: yield ([], obj) return {delimiter.join(path): value for path, value in recurse_keys(obj)} def http_setup_redirect( http: responses.RequestsMock, *methods: str, status_code: int = 301 ) -> None: redirect_uri_regex = re.compile(r"^(?Phttps?)://redirect\.(?P.*)$") def redirect_request_callback(request: PreparedRequest) -> HttpResponse: assert request.url redirect_uri_match = redirect_uri_regex.match(request.url) assert redirect_uri_match is not None redirect_uri = f"{redirect_uri_match.group('protocol')}://{redirect_uri_match.group('uri')}" return status_code, {"Location": redirect_uri}, b"" for method in methods: http.add_callback( method, redirect_uri_regex, callback=redirect_request_callback, ) @contextlib.contextmanager def switch_working_directory(path: Path, remove: bool = False) -> Iterator[Path]: original_cwd = Path.cwd() os.chdir(path) try: yield path finally: os.chdir(original_cwd) if remove: shutil.rmtree(path, ignore_errors=True) @contextlib.contextmanager def with_working_directory(source: Path, target: Path | None = None) -> Iterator[Path]: use_copy = target is not None if use_copy: assert target is not None shutil.copytree(source, target) with switch_working_directory(target or source, remove=use_copy) as path: yield path def set_keyring_backend(backend: KeyringBackend) -> None: """Clears availability cache and sets the specified keyring backend.""" PoetryKeyring.is_available.cache_clear() keyring.set_keyring(backend) def pbs_installer_supported_arch(architecture: str) -> bool: # Based on pbs_installer._versions and pbs_installer._utils.ARCH_MAPPING supported_archs = ["arm64", "aarch64", "amd64", "x86_64", "i686", "x86"] return architecture.lower() in supported_archs ================================================ FILE: tests/inspection/__init__.py ================================================ ================================================ FILE: tests/inspection/test_info.py ================================================ from __future__ import annotations import contextlib import shutil import uuid from subprocess import CalledProcessError from typing import TYPE_CHECKING from zipfile import ZipFile import pytest from build import BuildBackendException from build import ProjectBuilder from packaging.metadata import parse_email from pkginfo.distribution import NewMetadataVersion from poetry.inspection.info import PackageInfo from poetry.inspection.info import PackageInfoError from poetry.utils.env import VirtualEnv if TYPE_CHECKING: from collections.abc import Iterator from pathlib import Path from packaging.metadata import RawMetadata from pytest_mock import MockerFixture from tests.types import FixtureDirGetter from tests.types import SetProjectContext @pytest.fixture def demo_sdist(fixture_dir: FixtureDirGetter) -> Path: return fixture_dir("distributions") / "demo-0.1.0.tar.gz" @pytest.fixture def demo_wheel(fixture_dir: FixtureDirGetter) -> Path: return fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" @pytest.fixture def demo_wheel_metadata(demo_wheel: Path) -> RawMetadata: with ZipFile(demo_wheel) as zf: metadata, _ = parse_email(zf.read("demo-0.1.0.dist-info/METADATA")) return metadata @pytest.fixture def source_dir(tmp_path: Path) -> Path: path = tmp_path / "source" path.mkdir() return path @pytest.fixture def demo_setup(source_dir: Path) -> Path: setup_py = source_dir / "setup.py" setup_py.write_text( "from setuptools import setup; " 'setup(name="demo", ' 'version="0.1.0", ' 'install_requires=["package"])', encoding="utf-8", ) return source_dir @pytest.fixture def demo_setup_cfg(source_dir: Path) -> Path: setup_cfg = source_dir / "setup.cfg" setup_cfg.write_text( "\n".join( [ "[metadata]", "name = demo", "version = 0.1.0", "[options]", "install_requires = package", ] ), encoding="utf-8", ) return source_dir @pytest.fixture def demo_setup_complex(source_dir: Path) -> Path: setup_py = source_dir / "setup.py" setup_py.write_text( "from setuptools import setup; " 'setup(name="demo", ' 'version="0.1.0", ' 'install_requires=[i for i in ["package"]])', encoding="utf-8", ) return source_dir @pytest.fixture def demo_setup_complex_pep517_legacy(demo_setup_complex: Path) -> Path: pyproject_toml = demo_setup_complex / "pyproject.toml" pyproject_toml.write_text( '[build-system]\nrequires = ["setuptools", "wheel"]', encoding="utf-8" ) return demo_setup_complex @pytest.fixture def demo_setup_complex_calls_script( fixture_dir: FixtureDirGetter, source_dir: Path, tmp_path: Path ) -> Path: # make sure the scripts project is on the same drive (for Windows tests in CI) scripts_dir = tmp_path / "scripts" shutil.copytree(fixture_dir("scripts"), scripts_dir) pyproject = source_dir / "pyproject.toml" pyproject.write_text( f"""\ [build-system] requires = ["setuptools", "scripts @ {scripts_dir.as_uri()}"] build-backend = "setuptools.build_meta:__legacy__" """, encoding="utf-8", ) setup_py = source_dir / "setup.py" setup_py.write_text( """\ import subprocess from setuptools import setup if subprocess.call(["exit-code"]) != 42: raise RuntimeError("Wrong exit code.") setup(name="demo", version="0.1.0", install_requires=[i for i in ["package"]]) """, encoding="utf-8", ) return source_dir @pytest.fixture(autouse=True) def use_project_context(set_project_context: SetProjectContext) -> Iterator[None]: with set_project_context("sample_project"): yield def demo_check_info(info: PackageInfo, requires_dist: set[str] | None = None) -> None: assert info.name == "demo" assert info.version == "0.1.0" assert info.requires_dist if requires_dist: assert set(info.requires_dist) == requires_dist else: # Exact formatting various according to the exact mechanism used to extract the # metadata. assert set(info.requires_dist) in ( { 'cleo; extra == "foo"', "pendulum (>=1.4.4)", 'tomlkit; extra == "bar"', }, { 'cleo ; extra == "foo"', "pendulum (>=1.4.4)", 'tomlkit ; extra == "bar"', }, { 'cleo ; extra == "foo"', "pendulum>=1.4.4", 'tomlkit ; extra == "bar"', }, { "cleo ; extra == 'foo'", "pendulum (>=1.4.4)", "tomlkit ; extra == 'bar'", }, ) def test_info_from_sdist(demo_sdist: Path) -> None: info = PackageInfo.from_sdist(demo_sdist) demo_check_info(info) assert info._source_type == "file" assert info._source_url == demo_sdist.resolve().as_posix() def test_info_from_sdist_no_pkg_info(fixture_dir: FixtureDirGetter) -> None: path = fixture_dir("distributions") / "demo_no_pkg_info-0.1.0.tar.gz" info = PackageInfo.from_sdist(path) demo_check_info(info) assert info._source_type == "file" assert info._source_url == path.resolve().as_posix() def test_info_from_wheel(demo_wheel: Path) -> None: info = PackageInfo.from_wheel(demo_wheel) demo_check_info(info) assert info._source_type == "file" assert info._source_url == demo_wheel.resolve().as_posix() @pytest.mark.parametrize("version", ["23", "24", "299"]) def test_info_from_wheel_metadata_versions( version: str, fixture_dir: FixtureDirGetter ) -> None: path = ( fixture_dir("distributions") / f"demo_metadata_version_{version}-0.1.0-py2.py3-none-any.whl" ) with ( pytest.warns(NewMetadataVersion) if version == "299" else contextlib.nullcontext() ): info = PackageInfo.from_wheel(path) demo_check_info(info) assert info._source_type == "file" assert info._source_url == path.resolve().as_posix() def test_info_from_wheel_metadata_version_unknown( fixture_dir: FixtureDirGetter, ) -> None: path = ( fixture_dir("distributions") / "demo_metadata_version_unknown-0.1.0-py2.py3-none-any.whl" ) with pytest.warns(NewMetadataVersion), pytest.raises(PackageInfoError) as e: PackageInfo.from_wheel(path) assert "Unknown metadata version: 999.3" in str(e.value) def test_info_from_wheel_metadata(demo_wheel_metadata: RawMetadata) -> None: info = PackageInfo.from_metadata(demo_wheel_metadata) demo_check_info(info) assert info.requires_python == ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" assert info._source_type is None assert info._source_url is None def test_info_from_wheel_metadata_incomplete() -> None: """ To avoid differences in cached metadata, it is important that the representation of missing fields does not change! """ metadata, _ = parse_email(b"Metadata-Version: 2.1\nName: demo\nVersion: 0.1.0\n") info = PackageInfo.from_metadata(metadata) assert info.name == "demo" assert info.version == "0.1.0" assert info.summary is None assert info.requires_dist is None assert info.requires_python is None def test_info_from_bdist(demo_wheel: Path) -> None: info = PackageInfo.from_bdist(demo_wheel) demo_check_info(info) assert info._source_type == "file" assert info._source_url == demo_wheel.resolve().as_posix() def test_info_from_poetry_directory(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_directory(fixture_dir("inspection") / "demo") demo_check_info(info) def test_info_from_poetry_directory_fallback_on_poetry_create_error( mocker: MockerFixture, fixture_dir: FixtureDirGetter ) -> None: mock_create_poetry = mocker.patch( "poetry.inspection.info.Factory.create_poetry", side_effect=RuntimeError ) mock_get_poetry_package = mocker.spy(PackageInfo, "_get_poetry_package") mock_get_pep517_metadata = mocker.patch( "poetry.inspection.info.get_pep517_metadata" ) PackageInfo.from_directory(fixture_dir("inspection") / "demo_poetry_package") assert mock_create_poetry.call_count == 1 assert mock_get_poetry_package.call_count == 1 assert mock_get_pep517_metadata.call_count == 1 def test_info_from_requires_txt(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_metadata_directory( fixture_dir("inspection") / "demo_only_requires_txt.egg-info" ) assert info is not None demo_check_info(info) def test_info_no_setup_pkg_info_no_deps(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_metadata_directory( fixture_dir("inspection") / "demo_no_setup_pkg_info_no_deps" ) assert info is not None assert info.name == "demo" assert info.version == "0.1.0" assert info.requires_dist is None def test_info_no_setup_pkg_info_no_deps_for_sure(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_metadata_directory( fixture_dir("inspection") / "demo_no_setup_pkg_info_no_deps_for_sure", ) assert info is not None assert info.name == "demo" assert info.version == "0.1.0" assert info.requires_dist == [] def test_info_no_setup_pkg_info_no_deps_dynamic(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_metadata_directory( fixture_dir("inspection") / "demo_no_setup_pkg_info_no_deps_dynamic", ) assert info is not None assert info.name == "demo" assert info.version == "0.1.0" assert info.requires_dist is None def test_info_setup_simple(mocker: MockerFixture, demo_setup: Path) -> None: spy = mocker.spy(VirtualEnv, "run") info = PackageInfo.from_directory(demo_setup) assert spy.call_count == 6 demo_check_info(info, requires_dist={"package"}) def test_info_setup_complex(demo_setup_complex: Path) -> None: info = PackageInfo.from_directory(demo_setup_complex) demo_check_info(info, requires_dist={"package"}) def test_info_setup_complex_pep517_error( mocker: MockerFixture, demo_setup_complex: Path ) -> None: output = uuid.uuid4().hex mocker.patch( "build.ProjectBuilder.from_isolated_env", autospec=True, side_effect=BuildBackendException(CalledProcessError(1, "mock", output=output)), ) with pytest.raises(PackageInfoError) as exc: PackageInfo.from_directory(demo_setup_complex) text = str(exc.value) assert "Command 'mock' returned non-zero exit status 1." in text assert output in text assert ( "This error originates from the build backend, and is likely not a problem with poetry" in text ) def test_info_setup_complex_pep517_legacy( demo_setup_complex_pep517_legacy: Path, ) -> None: info = PackageInfo.from_directory(demo_setup_complex_pep517_legacy) demo_check_info(info, requires_dist={"package"}) def test_info_setup_complex_calls_script(demo_setup_complex_calls_script: Path) -> None: """Building the project requires calling a script from its build_requires.""" info = PackageInfo.from_directory(demo_setup_complex_calls_script) demo_check_info(info, requires_dist={"package"}) @pytest.mark.parametrize("missing", ["version", "name"]) def test_info_setup_missing_mandatory_should_trigger_pep517( mocker: MockerFixture, source_dir: Path, missing: str ) -> None: setup = "from setuptools import setup; " setup += "setup(" setup += 'name="demo", ' if missing != "name" else "" setup += 'version="0.1.0", ' if missing != "version" else "" setup += 'install_requires=["package"]' setup += ")" setup_py = source_dir / "setup.py" setup_py.write_text(setup, encoding="utf-8") spy = mocker.spy(ProjectBuilder, "from_isolated_env") _ = PackageInfo.from_directory(source_dir) assert spy.call_count == 1 def test_info_prefer_poetry_config_over_egg_info(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_directory( fixture_dir("inspection") / "demo_with_obsolete_egg_info" ) demo_check_info(info) ================================================ FILE: tests/inspection/test_lazy_wheel.py ================================================ from __future__ import annotations import re from enum import IntEnum from pathlib import Path from typing import TYPE_CHECKING from typing import Protocol from urllib.parse import urlparse import pytest import requests import responses from requests import codes from poetry.inspection.lazy_wheel import HTTPRangeRequestNotRespectedError from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupportedError from poetry.inspection.lazy_wheel import InvalidWheelError from poetry.inspection.lazy_wheel import LazyWheelUnsupportedError from poetry.inspection.lazy_wheel import metadata_from_wheel_url from tests.helpers import http_setup_redirect if TYPE_CHECKING: from pytest_mock import MockerFixture from requests import PreparedRequest from tests.types import FixtureDirGetter from tests.types import HttpRequestCallback from tests.types import HttpRequestCallbackWrapper from tests.types import HttpResponse from tests.types import PackageDistributionLookup class RequestCallbackFactory(Protocol): def __call__( self, *, accept_ranges: str | None = "bytes", negative_offset_error: tuple[int, bytes] | None = None, ignore_accept_ranges: bool = False, ) -> HttpRequestCallback: ... class AssertMetadataFromWheelUrl(Protocol): def __call__( self, *, accept_ranges: str | None = "bytes", negative_offset_error: tuple[int, bytes] | None = None, expected_requests: int = 3, request_callback_wrapper: HttpRequestCallbackWrapper | None = None, redirect: bool = True, ) -> None: ... class NegativeOffsetFailure(IntEnum): # numbers must be negative to avoid conflicts with HTTP status codes as_positive = -1 # JFrog Artifactory bug (RTDEV-38572) one_more = -2 # JFrog Artifactory bug (one more byte than requested) def build_head_response( accept_ranges: str | None, content_length: int, response_headers: dict[str, str] ) -> HttpResponse: response_headers["Content-Length"] = str(content_length) if accept_ranges: response_headers["Accept-Ranges"] = accept_ranges return 200, response_headers, b"" def build_partial_response( rng: str, wheel_bytes: bytes, response_headers: dict[str, str], *, negative_offset_failure: NegativeOffsetFailure | None = None, ) -> HttpResponse: status_code = 206 response_headers["Accept-Ranges"] = "bytes" total_length = len(wheel_bytes) if rng.startswith("-"): # negative offset offset = int(rng) if negative_offset_failure == NegativeOffsetFailure.as_positive: # some servers interpret a negative offset like "-10" as "0-10" start = 0 end = min(-offset, total_length - 1) body = wheel_bytes[start : end + 1] elif negative_offset_failure == NegativeOffsetFailure.one_more: # https://github.com/python-poetry/poetry/issues/9056#issuecomment-1973273721 offset -= 1 # one more byte start = total_length + offset # negative start of content range possible! end = total_length - 1 body = wheel_bytes[offset:] response_headers["Content-Length"] = str(-offset) # just wrong... else: start = total_length + offset if start < 0: # wheel is smaller than initial chunk size response_headers["Content-Length"] = str(len(wheel_bytes)) return 200, response_headers, wheel_bytes end = total_length - 1 body = wheel_bytes[offset:] else: # range with start and end start, end = map(int, rng.split("-")) body = wheel_bytes[start : end + 1] response_headers["Content-Range"] = f"bytes {start}-{end}/{total_length}" if "Content-Length" not in response_headers: response_headers["Content-Length"] = str(len(body)) return status_code, response_headers, body @pytest.fixture def handle_request_factory( fixture_dir: FixtureDirGetter, package_distribution_lookup: PackageDistributionLookup, ) -> RequestCallbackFactory: def _factory( *, accept_ranges: str | None = "bytes", negative_offset_error: tuple[int, bytes] | None = None, ignore_accept_ranges: bool = False, ) -> HttpRequestCallback: def handle_request(request: PreparedRequest) -> HttpResponse: assert request.url name = Path(urlparse(request.url).path).name wheel = package_distribution_lookup(name) or package_distribution_lookup( "demo-0.1.0-py2.py3-none-any.whl" ) if not wheel: return 404, {}, b"Not Found" wheel_bytes = wheel.read_bytes() response_headers: dict[str, str] = {} if request.method == "HEAD": return build_head_response( accept_ranges, len(wheel_bytes), response_headers ) rng = request.headers.get("Range", "=").split("=")[1] negative_offset_failure = None if negative_offset_error and rng.startswith("-"): if negative_offset_error[0] == codes.requested_range_not_satisfiable: response_headers["Content-Range"] = f"bytes */{len(wheel_bytes)}" if negative_offset_error[0] == NegativeOffsetFailure.as_positive: negative_offset_failure = NegativeOffsetFailure.as_positive elif negative_offset_error[0] == NegativeOffsetFailure.one_more: negative_offset_failure = NegativeOffsetFailure.one_more else: response_headers["Content-Length"] = str( len(negative_offset_error[1]) ) return ( negative_offset_error[0], response_headers, negative_offset_error[1], ) if accept_ranges == "bytes" and rng and not ignore_accept_ranges: return build_partial_response( rng, wheel_bytes, response_headers, negative_offset_failure=negative_offset_failure, ) status_code = 200 body = wheel_bytes response_headers["Content-Length"] = str(len(body)) return status_code, response_headers, body return handle_request return _factory @pytest.fixture def assert_metadata_from_wheel_url( http: responses.RequestsMock, handle_request_factory: RequestCallbackFactory, ) -> AssertMetadataFromWheelUrl: def _assertion( *, accept_ranges: str | None = "bytes", negative_offset_error: tuple[int, bytes] | None = None, expected_requests: int = 3, request_callback_wrapper: HttpRequestCallbackWrapper | None = None, redirect: bool = False, ) -> None: http.reset() domain = ( f"lazy-wheel-{negative_offset_error[0] if negative_offset_error else 0}.com" ) uri_regex = re.compile(f"^https://{domain}/.*$") request_callback = handle_request_factory( accept_ranges=accept_ranges, negative_offset_error=negative_offset_error ) if request_callback_wrapper is not None: request_callback = request_callback_wrapper(request_callback) http.add_callback(responses.GET, uri_regex, callback=request_callback) http.add_callback(responses.HEAD, uri_regex, callback=request_callback) if redirect: http_setup_redirect(http, responses.GET, responses.HEAD) url_prefix = "redirect." if redirect else "" url = f"https://{url_prefix}{domain}/poetry_core-1.5.0-py3-none-any.whl" metadata = metadata_from_wheel_url("poetry-core", url, requests.Session()) assert metadata["name"] == "poetry-core" assert metadata["version"] == "1.5.0" assert metadata["author"] == "Sébastien Eustace" assert metadata["requires_dist"] == [ 'importlib-metadata (>=1.7.0) ; python_version < "3.8"' ] assert len(http.calls) == expected_requests return _assertion @pytest.mark.parametrize( "negative_offset_error", [ None, (codes.not_found, b"Not found"), # Nexus (codes.method_not_allowed, b"Method not allowed"), (codes.requested_range_not_satisfiable, b"Requested range not satisfiable"), (codes.internal_server_error, b"Internal server error"), # GAR (codes.not_implemented, b"Unsupported client range"), # PyPI (NegativeOffsetFailure.as_positive, b"handle negative offset as positive"), (NegativeOffsetFailure.one_more, b"one more byte than requested"), ], ) def test_metadata_from_wheel_url( assert_metadata_from_wheel_url: AssertMetadataFromWheelUrl, negative_offset_error: tuple[int, bytes] | None, ) -> None: # negative offsets supported: # 1. end of central directory # 2. whole central directory # 3. METADATA file # negative offsets not supported: # 1. failed range request # 2. HEAD request # 3.-5. see negative offsets 1.-3. expected_requests = 3 if negative_offset_error: if negative_offset_error[0] in { codes.requested_range_not_satisfiable, NegativeOffsetFailure.as_positive, NegativeOffsetFailure.one_more, }: expected_requests += 1 else: expected_requests += 2 assert_metadata_from_wheel_url( negative_offset_error=negative_offset_error, expected_requests=expected_requests ) # second wheel -> one less request if negative offsets are not supported expected_requests = min(expected_requests, 4) assert_metadata_from_wheel_url( negative_offset_error=negative_offset_error, expected_requests=expected_requests ) def test_metadata_from_wheel_url_416_missing_content_range( assert_metadata_from_wheel_url: AssertMetadataFromWheelUrl, ) -> None: def request_callback_wrapper( request_callback: HttpRequestCallback, ) -> HttpRequestCallback: def _wrapped(request: PreparedRequest) -> HttpResponse: status_code, response_headers, body = request_callback(request) return ( status_code, { header: response_headers[header] for header in response_headers if header.lower() != "content-range" }, body, ) return _wrapped assert_metadata_from_wheel_url( negative_offset_error=( codes.requested_range_not_satisfiable, b"Requested range not satisfiable", ), expected_requests=5, request_callback_wrapper=request_callback_wrapper, ) def test_metadata_from_wheel_url_with_redirect( assert_metadata_from_wheel_url: AssertMetadataFromWheelUrl, ) -> None: assert_metadata_from_wheel_url( negative_offset_error=None, expected_requests=6, redirect=True, ) def test_metadata_from_wheel_url_with_redirect_after_500( assert_metadata_from_wheel_url: AssertMetadataFromWheelUrl, ) -> None: assert_metadata_from_wheel_url( negative_offset_error=(codes.internal_server_error, b"Internal server error"), expected_requests=10, redirect=True, ) @pytest.mark.parametrize( ("negative_offset_failure", "expected_requests"), [ (None, 1), (NegativeOffsetFailure.as_positive, 1), (NegativeOffsetFailure.one_more, 2), ], ) def test_metadata_from_wheel_url_smaller_than_initial_chunk_size( http: responses.RequestsMock, handle_request_factory: RequestCallbackFactory, negative_offset_failure: NegativeOffsetFailure | None, expected_requests: int, ) -> None: domain = f"tiny-wheel-{str(negative_offset_failure).casefold()}.com" uri_regex = re.compile(f"^https://{domain}/.*$") request_callback = handle_request_factory( negative_offset_error=( (negative_offset_failure, b"") if negative_offset_failure else None ) ) http.add_callback(responses.GET, uri_regex, callback=request_callback) http.add_callback(responses.HEAD, uri_regex, callback=request_callback) url = f"https://{domain}/zipp-3.5.0-py3-none-any.whl" metadata = metadata_from_wheel_url("zipp", url, requests.Session()) assert metadata["name"] == "zipp" assert metadata["version"] == "3.5.0" assert metadata["author"] == "Jason R. Coombs" assert len(metadata["requires_dist"]) == 12 assert len(http.calls) == expected_requests @pytest.mark.parametrize("accept_ranges", [None, "none"]) def test_metadata_from_wheel_url_range_requests_not_supported_one_request( http: responses.RequestsMock, handle_request_factory: RequestCallbackFactory, accept_ranges: str | None, ) -> None: domain = "no-range-requests.com" uri_regex = re.compile(f"^https://{domain}/.*$") request_callback = handle_request_factory(accept_ranges=accept_ranges) http.add_callback(responses.GET, uri_regex, callback=request_callback) http.add_callback(responses.HEAD, uri_regex, callback=request_callback) url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" with pytest.raises(HTTPRangeRequestUnsupportedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) assert len(http.calls) == 1 assert http.calls[0].request.method == "GET" @pytest.mark.parametrize( "negative_offset_error", [ (codes.method_not_allowed, b"Method not allowed"), (codes.not_implemented, b"Unsupported client range"), ], ) def test_metadata_from_wheel_url_range_requests_not_supported_two_requests( http: responses.RequestsMock, handle_request_factory: RequestCallbackFactory, negative_offset_error: tuple[int, bytes], ) -> None: domain = f"no-negative-offsets-{negative_offset_error[0]}.com" uri_regex = re.compile(f"^https://{domain}/.*$") request_callback = handle_request_factory( accept_ranges=None, negative_offset_error=negative_offset_error ) http.add_callback(responses.GET, uri_regex, callback=request_callback) http.add_callback(responses.HEAD, uri_regex, callback=request_callback) url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" with pytest.raises(HTTPRangeRequestUnsupportedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) assert len(http.calls) == 2 assert http.calls[0].request.method == "GET" assert http.calls[1].request.method == "HEAD" def test_metadata_from_wheel_url_range_requests_supported_but_not_respected( http: responses.RequestsMock, handle_request_factory: RequestCallbackFactory, ) -> None: domain = "range-requests-not-respected.com" uri_regex = re.compile(f"^https://{domain}/.*$") request_callback = handle_request_factory( negative_offset_error=(codes.method_not_allowed, b"Method not allowed"), ignore_accept_ranges=True, ) http.add_callback(responses.GET, uri_regex, callback=request_callback) http.add_callback(responses.HEAD, uri_regex, callback=request_callback) url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" with pytest.raises(HTTPRangeRequestNotRespectedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) assert len(http.calls) == 3 assert http.calls[0].request.method == "GET" assert http.calls[1].request.method == "HEAD" assert http.calls[2].request.method == "GET" def test_metadata_from_wheel_url_invalid_wheel( http: responses.RequestsMock, handle_request_factory: RequestCallbackFactory, ) -> None: domain = "invalid-wheel.com" uri_regex = re.compile(f"^https://{domain}/.*$") request_callback = handle_request_factory() http.add_callback(responses.GET, uri_regex, callback=request_callback) http.add_callback(responses.HEAD, uri_regex, callback=request_callback) url = f"https://{domain}/demo_missing_dist_info-0.1.0-py2.py3-none-any.whl" with pytest.raises(InvalidWheelError): metadata_from_wheel_url("demo-missing-dist-info", url, requests.Session()) assert len(http.calls) == 1 assert http.calls[0].request.method == "GET" def test_metadata_from_wheel_url_handles_unexpected_errors( mocker: MockerFixture, ) -> None: mocker.patch( "poetry.inspection.lazy_wheel.LazyWheelOverHTTP.read_metadata", side_effect=RuntimeError(), ) with pytest.raises(LazyWheelUnsupportedError): metadata_from_wheel_url( "demo-missing-dist-info", "https://runtime-error.com/demo_missing_dist_info-0.1.0-py2.py3-none-any.whl", requests.Session(), ) ================================================ FILE: tests/installation/__init__.py ================================================ ================================================ FILE: tests/installation/conftest.py ================================================ from __future__ import annotations import pytest from packaging.tags import Tag from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import MockEnv @pytest.fixture() def env() -> MockEnv: return MockEnv( supported_tags=[ Tag("cp37", "cp37", "macosx_10_15_x86_64"), Tag("py3", "none", "any"), ] ) @pytest.fixture() def pool(legacy_repository_html: LegacyRepository) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(PyPiRepository(disable_cache=True)) pool.add_repository( LegacyRepository("foo", "https://legacy.foo.bar/simple/", disable_cache=True) ) pool.add_repository( LegacyRepository("foo2", "https://legacy.foo2.bar/simple/", disable_cache=True) ) return pool ================================================ FILE: tests/installation/fixtures/extras-with-dependencies.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "C" version = "1.0" description = "" optional = true python-versions = "*" groups = ["main"] markers = 'extra == "foo"' files = [] [package.dependencies] D = "^1.0" [[package]] name = "D" version = "1.1" description = "" optional = true python-versions = "*" groups = ["main"] markers = 'extra == "foo"' files = [] [extras] foo = ["C"] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/extras.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "C" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "D" version = "1.1" description = "" optional = true python-versions = "*" groups = ["main"] files = [] [extras] foo = ["D"] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/install-no-dev.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "B" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "C" version = "1.2" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/no-dependencies.test ================================================ package = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/remove.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/update-with-lock.test ================================================ [[package]] name = "A" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/update-with-locked-extras.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] "B" = {version = "^1.0", optional = true} "C" = {version = "^1.0", markers = "python_version == \"2.7\""} [package.extras] foo = ["B"] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "C" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'python_version == "2.7"' files = [] [[package]] name = "D" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-conditional-dependency.test ================================================ [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = ">=3.5" groups = ["main"] markers = 'python_version >= "3.5"' files = [] [metadata] python-versions = "~2.7 || ^3.4" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-conflicting-dependency-extras-root.test ================================================ [[package]] name = "conflicting-dep" version = "1.1.0" description = "" optional = true python-versions = "*" files = [ ] groups = [ "main" ] markers = "extra == \"extra-one\" and extra != \"extra-two\"" [[package]] name = "conflicting-dep" version = "1.2.0" description = "" optional = true python-versions = "*" files = [ ] groups = [ "main" ] markers = "extra != \"extra-one\" and extra == \"extra-two\"" [extras] extra-one = [ "conflicting-dep", "conflicting-dep" ] extra-two = [ "conflicting-dep", "conflicting-dep" ] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-conflicting-dependency-extras-transitive.test ================================================ [[package]] name = "conflicting-dep" version = "1.1.0" description = "" optional = true python-versions = "*" files = [ ] groups = [ "main" ] markers = "extra == \"root-extra-one\" and extra != \"root-extra-two\"" [[package]] name = "conflicting-dep" version = "1.2.0" description = "" optional = true python-versions = "*" files = [ ] groups = [ "main" ] markers = "extra != \"root-extra-one\" and extra == \"root-extra-two\"" [[package]] name = "intermediate-dep" version = "1.0.0" description = "" optional = true python-versions = "*" files = [ ] groups = [ "main" ] markers = "extra == \"root-extra-one\" and extra != \"root-extra-two\" or extra != \"root-extra-one\" and extra == \"root-extra-two\"" [[package.dependencies.conflicting-dep]] version = "1.2.0" optional = true markers = 'extra != "extra-one" and extra == "extra-two"' [[package.dependencies.conflicting-dep]] version = "1.1.0" optional = true markers = 'extra == "extra-one" and extra != "extra-two"' [package.extras] extra-one = [ "conflicting-dep (==1.1.0)", "conflicting-dep (==1.2.0)" ] extra-two = [ "conflicting-dep (==1.1.0)", "conflicting-dep (==1.2.0)" ] [extras] root-extra-one = [ "intermediate-dep", "intermediate-dep" ] root-extra-two = [ "intermediate-dep", "intermediate-dep" ] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-dependencies-differing-extras.test ================================================ [[package]] name = "demo" version = "1.0.0" description = "" optional = true python-versions = "*" files = [ ] groups = [ "main" ] markers = "extra == \"extra-one\" and extra != \"extra-two\" or extra != \"extra-one\" and extra == \"extra-two\"" [package.dependencies.transitive-dep-one] version = "1.1.0" optional = true markers = 'extra == "demo-extra-one" and extra != "demo-extra-two"' [package.dependencies.transitive-dep-two] version = "1.2.0" optional = true markers = 'extra != "demo-extra-one" and extra == "demo-extra-two"' [package.extras] demo-extra-one = [ "transitive-dep-one", "transitive-dep-two" ] demo-extra-two = [ "transitive-dep-one", "transitive-dep-two" ] [[package]] name = "transitive-dep-one" version = "1.1.0" description = "" optional = true python-versions = "*" files = [ ] groups = [ "main" ] markers = "extra == \"extra-one\" and extra != \"extra-two\"" [[package]] name = "transitive-dep-two" version = "1.2.0" description = "" optional = true python-versions = "*" files = [ ] groups = [ "main" ] markers = "extra != \"extra-one\" and extra == \"extra-two\"" [extras] extra-one = [ "demo", "demo" ] extra-two = [ "demo", "demo" ] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-dependencies-extras.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] C = {version = "^1.0", optional = true} [package.extras] foo = ["C (>=1.0,<2.0)"] [[package]] name = "C" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-dependencies-nested-extras.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] B = {version = "^1.0", optional = true, extras = ["c"]} [package.extras] b = ["B[c] (>=1.0,<2.0)"] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] C = {version = "^1.0", optional = true} [package.extras] c = ["C (>=1.0,<2.0)"] [[package]] name = "C" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-dependencies.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "B" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-directory-dependency-poetry-transitive.test ================================================ [[package]] description = "" name = "demo" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.1.0" groups = ["main"] [[package.files]] file = "demo-0.1.0-py2.py3-none-any.whl" hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" [package.dependencies] pendulum = ">=1.4.4" [package.extras] bar = ["tomlkit"] foo = ["cleo"] [package.source] type = "file" url = "../distributions/demo-0.1.0-py2.py3-none-any.whl" [[package]] description = "This is a description" develop = false name = "inner-directory-project" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.2.4" [package.source] type = "directory" url = "project_with_transitive_file_dependencies/inner-directory-project" [[package]] description = "" name = "pendulum" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.4.4" [[package]] description = "This is a description" develop = false name = "project-with-extras" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.2.3" [package.extras] extras-a = ["pendulum (>=1.4.4)"] extras-b = ["cachy (>=0.2.0)"] [package.source] type = "directory" url = "../project_with_extras" [[package]] description = "This is a description" develop = false name = "project-with-transitive-directory-dependencies" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.2.3" [package.dependencies] project-with-extras = { "path" = "../../project_with_extras" } project-with-transitive-file-dependencies = { "path" = "../project_with_transitive_file_dependencies" } [package.source] type = "directory" url = "project_with_transitive_directory_dependencies" [[package]] description = "This is a description" develop = false name = "project-with-transitive-file-dependencies" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.2.3" [package.dependencies] demo = { "path" = "../../distributions/demo-0.1.0-py2.py3-none-any.whl" } inner-directory-project = { "path" = "inner-directory-project" } [package.source] type = "directory" url = "project_with_transitive_file_dependencies" [metadata] content-hash = "123456789" lock-version = "2.1" python-versions = "*" ================================================ FILE: tests/installation/fixtures/with-directory-dependency-poetry.test ================================================ [[package]] description = "" name = "pendulum" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.4.4" [[package]] description = "This is a description" develop = false name = "project-with-extras" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.2.3" [package.dependencies] pendulum = {version = ">=1.4.4", optional = true} [package.extras] extras-a = ["pendulum (>=1.4.4)"] extras-b = ["cachy (>=0.2.0)"] [package.source] type = "directory" url = "tests/fixtures/project_with_extras" [metadata] content-hash = "123456789" lock-version = "2.1" python-versions = "*" ================================================ FILE: tests/installation/fixtures/with-directory-dependency-setuptools.test ================================================ [[package]] name = "cachy" version = "0.2.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "pendulum" version = "1.4.4" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "project-with-setup" version = "0.1.2" develop = false description = "Demo project." optional = false python-versions = "*" groups = ["main"] files = [] [package.source] type = "directory" url = "project" [package.dependencies] cachy = {version = ">=0.2.0", extras = ["msgpack"]} pendulum = ">=1.4.4" [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-duplicate-dependencies-update.test ================================================ [[package]] name = "A" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] B = "^2.0" [[package]] name = "B" version = "2.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] C = "1.5" [[package]] name = "C" version = "1.5" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-duplicate-dependencies.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] B = [ {version = ">=1.0,<2.0", markers = "python_version < \"4.0\""}, {version = ">=2.0,<3.0", markers = "python_version >= \"4.0\""}, ] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'python_version < "4.0"' files = [] [package.dependencies] C = "1.2" [[package]] name = "B" version = "2.0" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'python_version >= "4.0"' files = [] [package.dependencies] C = "1.5" [[package]] name = "C" version = "1.2" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'python_version < "4.0"' files = [] [[package]] name = "C" version = "1.5" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'python_version >= "4.0"' files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-exclusive-extras.test ================================================ [[package]] name = "torch" version = "1.11.0+cpu" description = "" optional = true python-versions = "*" files = [] groups = [ "main" ] markers = "extra == \"cpu\" and extra != \"cuda\"" [package.source] reference = "pytorch-cpu" type = "legacy" url = "https://download.pytorch.org/whl/cpu" [[package]] name = "torch" version = "1.11.0+cuda" description = "" optional = true python-versions = "*" files = [] groups = [ "main" ] markers = "extra != \"cpu\" and extra == \"cuda\"" [package.source] reference = "pytorch-cuda" type = "legacy" url = "https://download.pytorch.org/whl/cuda" [extras] cpu = ["torch", "torch"] cuda = ["torch", "torch"] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-file-dependency-transitive.test ================================================ [[package]] description = "" name = "demo" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.1.0" groups = ["main"] [[package.files]] file = "demo-0.1.0-py2.py3-none-any.whl" hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" [package.dependencies] pendulum = ">=1.4.4" [package.extras] bar = ["tomlkit"] foo = ["cleo"] [package.source] type = "file" url = "../distributions/demo-0.1.0-py2.py3-none-any.whl" [[package]] description = "This is a description" develop = false name = "inner-directory-project" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.2.4" [package.source] type = "directory" url = "project_with_transitive_file_dependencies/inner-directory-project" [[package]] description = "" name = "pendulum" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.4.4" [[package]] description = "This is a description" develop = false name = "project-with-transitive-file-dependencies" optional = false python-versions = "*" groups = ["main"] files = [] version = "1.2.3" [package.dependencies] demo = { "path" = "../../distributions/demo-0.1.0-py2.py3-none-any.whl" } inner-directory-project = { "path" = "inner-directory-project" } [package.source] type = "directory" url = "project_with_transitive_file_dependencies" [metadata] content-hash = "123456789" lock-version = "2.1" python-versions = "*" ================================================ FILE: tests/installation/fixtures/with-file-dependency.test ================================================ [[package]] name = "demo" version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] [[package.files]] file = "demo-0.1.0-py2.py3-none-any.whl" hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" [package.source] type = "file" url = "tests/fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" [package.dependencies] pendulum = ">=1.4.4" [package.extras] bar = ["tomlkit"] foo = ["cleo"] [[package]] name = "pendulum" version = "1.4.4" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-multiple-updates.test ================================================ [[package]] name = "A" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] B = ">=1.0.1" C = [ {version = ">=1.0,<2.0", markers = "python_version == \"2.7\""}, {version = ">=2.0,<3.0", markers = "python_version >= \"3.4\" and python_version < \"4.0\""}, ] [[package]] name = "B" version = "1.1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "C" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'python_version == "2.7"' files = [] [[package]] name = "C" version = "2.0" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'python_version >= "3.4"' files = [] [metadata] python-versions = "~2.7 || ^3.4" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-optional-dependencies.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = true python-versions = "*" groups = ["main"] markers = 'extra == "foo"' files = [] [[package]] name = "C" version = "1.3" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] D = "^1.2" [[package]] name = "D" version = "1.4" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [extras] foo = ["A"] [metadata] python-versions = "~2.7 || ^3.4" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-platform-dependencies.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = true python-versions = "*" groups = ["main"] markers = 'extra == "foo"' files = [] [[package]] name = "B" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'sys_platform == "custom"' files = [] [[package]] name = "C" version = "1.3" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'sys_platform == "darwin"' files = [] [package.dependencies] D = "^1.2" [[package]] name = "D" version = "1.4" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'sys_platform == "darwin"' files = [] [extras] foo = ["A"] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-prereleases.test ================================================ [[package]] name = "A" version = "1.0a2" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "B" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-pypi-repository.test ================================================ # This file is automatically @generated by Poetry 1.9.0.dev0 and should not be changed by hand. [[package]] name = "attrs" version = "17.4.0" description = "Classes Without Boilerplate" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "attrs-17.4.0-py2.py3-none-any.whl", hash = "sha256:1fbfc10ebc8c876dcbab17f016b80ae1a4f0c1413461a695871427960795beb4"}, {file = "attrs-17.4.0.tar.gz", hash = "sha256:eb7536a1e6928190b3008c5b350bdf9850d619fff212341cd096f87a27a5e564"}, ] [package.extras] dev = ["coverage", "hypothesis", "pympler", "pytest", "six", "sphinx", "zope.interface", "zope.interface"] docs = ["sphinx", "zope.interface"] tests = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface"] [[package]] name = "colorama" version = "0.3.9" description = "Cross-platform colored terminal text." optional = false python-versions = "*" groups = ["dev"] markers = 'sys_platform == "win32"' files = [ {file = "colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:78a441d2e984c790526cdef1cfd8415a366979ef5b3186771a055b35886953bf"}, {file = "colorama-0.3.9.tar.gz", hash = "sha256:4c5a15209723ce1330a5c193465fe221098f761e9640d823a2ce7c03f983137f"}, ] [[package]] name = "more-itertools" version = "4.1.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "more-itertools-4.1.0.tar.gz", hash = "sha256:bab2dc6f4be8f9a4a72177842c5283e2dff57c167439a03e3d8d901e854f0f2e"}, {file = "more_itertools-4.1.0-py2-none-any.whl", hash = "sha256:0f461c2cd4ec16611396f9ee57f40433de3d59e95475d84c0c829cde02f746cd"}, {file = "more_itertools-4.1.0-py3-none-any.whl", hash = "sha256:580b6002d1f28feb5bcb8303278d59cf17dfbd19a63a5c2375112dae72c9bf98"}, ] [package.dependencies] six = ">=1.0.0,<2.0.0" [[package]] name = "pluggy" version = "0.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["dev"] files = [ {file = "pluggy-0.6.0-py2-none-any.whl", hash = "sha256:f5f767d398f18aa177976bf9c4d0c05d96487a7d8f07062251585803aaf56246"}, {file = "pluggy-0.6.0-py3-none-any.whl", hash = "sha256:d34798b80853ab688de1a3ca5b99ba4de91c459c19c76a555dc939979ae67eb0"}, {file = "pluggy-0.6.0.tar.gz", hash = "sha256:a982e208d054867661d27c6d2a86b17ba05fbb6b1bdc01f42660732dd107f865"}, ] [[package]] name = "py" version = "1.5.3" description = "library with cross-python path, ini-parsing, io, code, log facilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["dev"] files = [ {file = "py-1.5.3-py2.py3-none-any.whl", hash = "sha256:ef4a94f47156178e42ef8f2b131db420e0f4b6aa0b3936b6dbde6ad6487476a5"}, {file = "py-1.5.3.tar.gz", hash = "sha256:2df2c513c3af11de15f58189ba5539ddc4768c6f33816dc5c03950c8bd6180fa"}, ] [[package]] name = "pytest" version = "3.5.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["dev"] files = [ {file = "pytest-3.5.1-py2.py3-none-any.whl", hash = "sha256:d327df3686046c5b374a9776d9e11606f7dba6fb3db5cf5d60ebc78a31e0768e"}, {file = "pytest-3.5.1.tar.gz", hash = "sha256:b8fe151f3e181801dd38583a1c03818fbc662a8fce96c9063a0af624613e78f8"}, ] [package.dependencies] attrs = ">=17.4.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} more-itertools = ">=4.0.0" pluggy = ">=0.5,<0.7" py = ">=1.5.0" setuptools = "*" six = ">=1.10.0" [[package]] name = "setuptools" version = "67.6.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, {file = "setuptools-67.6.1.tar.gz", hash = "sha256:a737d365c957dd3fced9ddd246118e95dce7a62c3dc49f37e7fdd9e93475d785"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.11.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "six-1.11.0-py2.py3-none-any.whl", hash = "sha256:534e9875e44a507adec601c29b3cbd2ca6dae7df92bf3dd20c7289b2f99f7466"}, {file = "six-1.11.0.tar.gz", hash = "sha256:268a4ccb159c1a2d2c79336b02e75058387b0cdbb4cea2f07846a758f48a356d"}, ] [metadata] lock-version = "2.1" python-versions = ">=3.7" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-python-versions.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "B" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "C" version = "1.2" description = "" optional = false python-versions = "~2.7 || ^3.3" groups = ["main"] files = [] [metadata] python-versions = "~2.7 || ^3.4" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-same-version-url-dependencies.test ================================================ [[package]] name = "demo" version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] markers = 'sys_platform == "win32"' files = [ {file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"} ] [package.source] type = "url" url = "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" [package.dependencies] pendulum = ">=1.4.4" [package.extras] bar = ["tomlkit"] foo = ["cleo"] [[package]] name = "demo" version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] markers = 'sys_platform == "linux"' files = [ {file = "demo-0.1.0.tar.gz", hash = "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad"} ] [package.source] type = "url" url = "https://files.pythonhosted.org/distributions/demo-0.1.0.tar.gz" [package.dependencies] pendulum = ">=1.4.4" [package.extras] bar = ["tomlkit"] foo = ["cleo"] [[package]] name = "pendulum" version = "1.4.4" description = "" optional = false python-versions = "*" groups = ["main"] markers = 'sys_platform == "linux" or sys_platform == "win32"' files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-all-deep.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] a = {version = "1.0", extras = ["all"]} [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-all-top.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-b-markers.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install] ; python_version < \"3.9\""] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-deep.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] a = "1.0" [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-download-deep.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] a = {version = "1.0", extras = ["download"]} [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-download-top.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-install-deep.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] a = {version = "1.0", extras = ["install"]} [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-install-download-deep.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] a = {version = "1.0", extras = ["download", "install"]} [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-install-download-top.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-install-top.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-nested-deep.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] a = {version = "1.0", extras = ["nested"]} [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-nested-top.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] download-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"download\""} install-package = {version = ">=1.0,<2.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [[package]] name = "download-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "install-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-self-referencing-extras-top.test ================================================ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.extras] all = ["a[download,install]"] download = ["download-package (>=1.0,<2.0)"] install = ["install-package (>=1.0,<2.0)"] nested = ["a[all]"] py = ["a[py310,py38]"] py310 = ["py310-package (>=1.0,<2.0) ; python_version > \"3.8\""] py38 = ["py38-package (>=1.0,<2.0) ; python_version == \"3.8\""] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-sub-dependencies.test ================================================ [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] D = "^1.0" [[package]] name = "B" version = "1.1" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] C = "~1.2" [[package]] name = "C" version = "1.2" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "D" version = "1.3" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-url-dependency.test ================================================ [[package]] name = "demo" version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] files = [ {file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"} ] [package.source] type = "url" url = "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" [package.dependencies] pendulum = ">=1.4.4" [package.extras] bar = ["tomlkit"] foo = ["cleo"] [[package]] name = "pendulum" version = "1.4.4" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-vcs-dependency-with-extras.test ================================================ [[package]] name = "demo" version = "0.1.2" description = "" optional = false python-versions = "*" groups = ["main"] files = [] develop = false [package.dependencies] cleo = {version = "*", optional = true, markers = "extra == \"foo\""} pendulum = ">=1.4.4" [package.extras] foo = ["cleo"] [package.source] type = "git" url = "https://github.com/demo/demo.git" reference = "master" resolved_reference = "123456" [[package]] name = "pendulum" version = "1.4.4" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "cleo" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-vcs-dependency-without-ref.test ================================================ [[package]] name = "demo" version = "0.1.2" description = "" optional = false python-versions = "*" groups = ["main"] files = [] develop = false [package.dependencies] pendulum = ">=1.4.4" [package.source] type = "git" url = "https://github.com/demo/demo.git" reference = "HEAD" resolved_reference = "123456" [[package]] name = "pendulum" version = "1.4.4" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/fixtures/with-wheel-dependency-no-requires-dist.test ================================================ [[package]] name = "demo" version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] [[package.files]] file = "demo-0.1.0-py2.py3-none-any.whl" hash = "sha256:c25eb81459126848a1788eb3520d1a32014eb51ce3d3bae88c56bfdde4ce02db" [package.source] type = "file" url = "tests/fixtures/wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.whl" [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" ================================================ FILE: tests/installation/test_chef.py ================================================ from __future__ import annotations import os import shutil import tempfile from pathlib import Path from typing import TYPE_CHECKING from zipfile import ZipFile import pytest from build import ProjectBuilder from poetry.core.packages.utils.link import Link from poetry.factory import Factory from poetry.installation.chef import Chef from poetry.repositories import RepositoryPool from poetry.utils.env import EnvManager if TYPE_CHECKING: from pytest_mock import MockerFixture from poetry.repositories.pypi_repository import PyPiRepository from poetry.utils.cache import ArtifactCache from tests.conftest import Config from tests.types import FixtureDirGetter @pytest.fixture() def pool(pypi_repository: PyPiRepository) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(pypi_repository) return pool @pytest.fixture(autouse=True) def setup(mocker: MockerFixture, pool: RepositoryPool) -> None: mocker.patch.object(Factory, "create_pool", return_value=pool) def test_prepare_sdist( config: Config, config_cache_dir: Path, artifact_cache: ArtifactCache, fixture_dir: FixtureDirGetter, ) -> None: chef = Chef( artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) archive = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve() destination = artifact_cache.get_cache_directory_for_link(Link(archive.as_uri())) wheel = chef.prepare(archive) assert wheel.parent == destination assert wheel.name == "demo-0.1.0-py3-none-any.whl" def test_prepare_directory( config: Config, config_cache_dir: Path, artifact_cache: ArtifactCache, fixture_dir: FixtureDirGetter, ) -> None: chef = Chef( artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) archive = fixture_dir("simple_project_legacy").resolve() wheel = chef.prepare(archive) assert wheel.name == "simple_project-1.2.3-py2.py3-none-any.whl" assert wheel.parent.parent == Path(tempfile.gettempdir()) # cleanup generated tmp dir artifact os.unlink(wheel) def test_prepare_directory_with_extensions( config: Config, config_cache_dir: Path, artifact_cache: ArtifactCache, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: env = EnvManager.get_system_env() chef = Chef(artifact_cache, env, Factory.create_pool(config)) archive = shutil.copytree( fixture_dir("extended_with_no_setup").resolve(), tmp_path / "project" ) wheel = chef.prepare(archive) assert wheel.parent.parent == Path(tempfile.gettempdir()) assert wheel.name == f"extended-0.1-{env.supported_tags[0]}.whl" # cleanup generated tmp dir artifact os.unlink(wheel) def test_prepare_directory_editable( config: Config, config_cache_dir: Path, artifact_cache: ArtifactCache, fixture_dir: FixtureDirGetter, ) -> None: chef = Chef( artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) archive = fixture_dir("simple_project_legacy").resolve() wheel = chef.prepare(archive, editable=True) assert wheel.parent.parent == Path(tempfile.gettempdir()) assert wheel.name == "simple_project-1.2.3-py2.py3-none-any.whl" with ZipFile(wheel) as z: assert "simple_project.pth" in z.namelist() # cleanup generated tmp dir artifact os.unlink(wheel) def test_prepare_directory_script( config: Config, config_cache_dir: Path, artifact_cache: ArtifactCache, fixture_dir: FixtureDirGetter, tmp_path: Path, mocker: MockerFixture, ) -> None: """ Building a project that requires calling a script from its build_requires. """ # make sure the scripts project is on the same drive (for Windows tests in CI) scripts_dir = tmp_path / "scripts" shutil.copytree(fixture_dir("scripts"), scripts_dir) orig_build_system_requires = ProjectBuilder.build_system_requires class CustomPropertyMock: def __get__( self, obj: ProjectBuilder, obj_type: type[ProjectBuilder] | None = None ) -> set[str]: assert isinstance(obj, ProjectBuilder) return { req.replace("", f"scripts @ {scripts_dir.as_uri()}") for req in orig_build_system_requires.fget(obj) # type: ignore[attr-defined] } mocker.patch( "build.ProjectBuilder.build_system_requires", new_callable=CustomPropertyMock, ) chef = Chef( artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) archive = shutil.copytree( fixture_dir("project_with_setup_calls_script").resolve(), tmp_path / "project" ) wheel = chef.prepare(archive) assert wheel.name == "project_with_setup_calls_script-0.1.2-py3-none-any.whl" assert wheel.parent.parent == Path(tempfile.gettempdir()) # cleanup generated tmp dir artifact os.unlink(wheel) ================================================ FILE: tests/installation/test_chooser.py ================================================ from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING import pytest from packaging.tags import Tag from poetry.core.packages.package import Package from poetry.console.exceptions import PoetryRuntimeError from poetry.installation.chooser import Chooser from poetry.repositories.legacy_repository import LegacyRepository from poetry.utils.env import MockEnv if TYPE_CHECKING: from poetry.core.packages.package import PackageFile from poetry.repositories.repository_pool import RepositoryPool from tests.conftest import Config from tests.types import DistributionHashGetter from tests.types import SpecializedLegacyRepositoryMocker JSON_FIXTURES = ( Path(__file__).parent.parent / "repositories" / "fixtures" / "pypi.org" / "json" ) LEGACY_FIXTURES = Path(__file__).parent.parent / "repositories" / "fixtures" / "legacy" def check_chosen_link_filename( env: MockEnv, source_type: str, pool: RepositoryPool, filename: str | None, config: Config | None = None, package_name: str = "pytest", package_version: str = "3.5.0", ) -> None: chooser = Chooser(pool, env, config) package = Package(package_name, package_version) if source_type == "legacy": package = Package( package.name, package.version.text, source_type="legacy", source_reference="foo", source_url="https://legacy.foo.bar/simple/", ) try: link = chooser.choose_for(package) except PoetryRuntimeError as e: if filename is None: assert ( str(e) == f"Unable to find installation candidates for {package.name} ({package.version})" ) else: pytest.fail("Package was not found") else: assert link.filename == filename @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_universal_wheel_link_if_available( env: MockEnv, source_type: str, pool: RepositoryPool, ) -> None: check_chosen_link_filename( env, source_type, pool, "pytest-3.5.0-py2.py3-none-any.whl" ) @pytest.mark.parametrize( ("policy", "filename"), [ (":all:", "pytest-3.5.0.tar.gz"), (":none:", "pytest-3.5.0-py2.py3-none-any.whl"), ("black", "pytest-3.5.0-py2.py3-none-any.whl"), ("pytest", "pytest-3.5.0.tar.gz"), ("pytest,black", "pytest-3.5.0.tar.gz"), ], ) @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_no_binary_policy( env: MockEnv, source_type: str, pool: RepositoryPool, policy: str, filename: str, config: Config, ) -> None: config.merge({"installer": {"no-binary": policy.split(",")}}) check_chosen_link_filename(env, source_type, pool, filename, config) @pytest.mark.parametrize( ("policy", "filename"), [ (":all:", "pytest-3.5.0-py2.py3-none-any.whl"), (":none:", "pytest-3.5.0-py2.py3-none-any.whl"), ("black", "pytest-3.5.0-py2.py3-none-any.whl"), ("pytest", "pytest-3.5.0-py2.py3-none-any.whl"), ("pytest,black", "pytest-3.5.0-py2.py3-none-any.whl"), ], ) @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_only_binary_policy( env: MockEnv, source_type: str, pool: RepositoryPool, policy: str, filename: str, config: Config, ) -> None: config.merge({"installer": {"only-binary": policy.split(",")}}) check_chosen_link_filename(env, source_type, pool, filename, config) @pytest.mark.parametrize( ("no_binary", "only_binary", "filename"), [ # no `no_binary` nor `only_binary` (":none:", ":none:", "pytest-3.5.0-py2.py3-none-any.whl"), ("black", "black", "pytest-3.5.0-py2.py3-none-any.whl"), # `no_binary` only (":all:", ":none:", "pytest-3.5.0.tar.gz"), ("pytest", "black", "pytest-3.5.0.tar.gz"), # `only_binary` only (":none:", ":all:", "pytest-3.5.0-py2.py3-none-any.whl"), ("black", "pytest", "pytest-3.5.0-py2.py3-none-any.whl"), # both `no_binary` and `only_binary` ("pytest", "pytest", None), (":all:", ":all:", None), ("pytest", ":all:", "pytest-3.5.0.tar.gz"), (":all:", "pytest", "pytest-3.5.0-py2.py3-none-any.whl"), # complex cases ("pytest,black", "pytest,black", None), ], ) @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_multiple_binary_policy( env: MockEnv, source_type: str, pool: RepositoryPool, no_binary: str, only_binary: str, filename: str | None, config: Config, ) -> None: config.merge( { "installer": { "no-binary": no_binary.split(","), "only-binary": only_binary.split(","), } } ) check_chosen_link_filename(env, source_type, pool, filename, config) @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_specific_python_universal_wheel_link_if_available( env: MockEnv, source_type: str, pool: RepositoryPool, ) -> None: check_chosen_link_filename( env, source_type, pool, "isort-4.3.4-py3-none-any.whl", None, "isort", "4.3.4" ) @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_system_specific_wheel_link_if_available( source_type: str, pool: RepositoryPool ) -> None: env = MockEnv( supported_tags=[Tag("cp37", "cp37m", "win32"), Tag("py3", "none", "any")] ) check_chosen_link_filename( env, source_type, pool, "PyYAML-3.13-cp37-cp37m-win32.whl", None, "pyyaml", "3.13.0", ) @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_sdist_if_no_compatible_wheel_link_is_available( env: MockEnv, source_type: str, pool: RepositoryPool, ) -> None: check_chosen_link_filename( env, source_type, pool, "PyYAML-3.13.tar.gz", None, "pyyaml", "3.13.0" ) @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_distributions_that_match_the_package_hashes( env: MockEnv, source_type: str, pool: RepositoryPool, dist_hash_getter: DistributionHashGetter, ) -> None: chooser = Chooser(pool, env) package = Package("isort", "4.3.4") files: list[PackageFile] = [ { "file": filename, "hash": f"sha256:{dist_hash_getter(filename).sha256}", } for filename in [ f"{package.name}-{package.version}.tar.gz", ] ] if source_type == "legacy": package = Package( package.name, package.version.text, source_type="legacy", source_reference="foo", source_url="https://legacy.foo.bar/simple/", ) package.files = files link = chooser.choose_for(package) assert link.filename == "isort-4.3.4.tar.gz" @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_yanked_if_no_others( env: MockEnv, source_type: str, pool: RepositoryPool, dist_hash_getter: DistributionHashGetter, ) -> None: chooser = Chooser(pool, env) package = Package("black", "21.11b0") files: list[PackageFile] = [ { "file": filename, "hash": (f"sha256:{dist_hash_getter(filename).sha256}"), } for filename in [f"{package.name}-{package.version}-py3-none-any.whl"] ] if source_type == "legacy": package = Package( package.name, package.version.text, source_type="legacy", source_reference="foo", source_url="https://legacy.foo.bar/simple/", ) package.files = files link = chooser.choose_for(package) assert link.filename == "black-21.11b0-py3-none-any.whl" assert link.yanked def test_chooser_does_not_choose_yanked_if_others( specialized_legacy_repository_mocker: SpecializedLegacyRepositoryMocker, pool: RepositoryPool, dist_hash_getter: DistributionHashGetter, ) -> None: chooser = Chooser(pool, MockEnv(supported_tags=[Tag("py2", "none", "any")])) repo = pool.repository("foo2") pool.remove_repository("foo2") assert isinstance(repo, LegacyRepository) pool.add_repository( specialized_legacy_repository_mocker("-partial-yank", repo.name, repo.url) ) package = Package("futures", "3.2.0") files: list[PackageFile] = [ { "file": filename, "hash": f"sha256:{dist_hash_getter(filename).sha256}", } for filename in [ f"{package.name}-{package.version}-py2-none-any.whl", f"{package.name}-{package.version}.tar.gz", ] ] package = Package( package.name, package.version.text, source_type="legacy", source_reference="foo", source_url="https://legacy.foo.bar/simple/", ) package_partial_yank = Package( package.name, package.version.text, source_type="legacy", source_reference="foo2", source_url="https://legacy.foo2.bar/simple/", ) package.files = files package_partial_yank.files = files link = chooser.choose_for(package) link_partial_yank = chooser.choose_for(package_partial_yank) assert link.filename == "futures-3.2.0-py2-none-any.whl" assert link_partial_yank.filename == "futures-3.2.0.tar.gz" @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_throws_an_error_if_package_hashes_do_not_match( env: MockEnv, source_type: None, pool: RepositoryPool, ) -> None: chooser = Chooser(pool, env) package = Package("isort", "4.3.4") files: list[PackageFile] = [ { "hash": ( "sha256:0000000000000000000000000000000000000000000000000000000000000000" ), "file": "isort-4.3.4.tar.gz", } ] if source_type == "legacy": package = Package( package.name, package.version.text, source_type="legacy", source_reference="foo", source_url="https://legacy.foo.bar/simple/", ) package.files = files with pytest.raises(PoetryRuntimeError) as e: chooser.choose_for(package) reason = f"Downloaded distributions for {package.name} ({package.version}) did not match any known checksums in your lock file." assert str(e.value) == reason text = e.value.get_text(debug=True, strip=True) assert reason in text assert files[0]["hash"] in text def test_chooser_md5_remote_fallback_to_sha256_inline_calculation( env: MockEnv, pool: RepositoryPool, dist_hash_getter: DistributionHashGetter ) -> None: chooser = Chooser(pool, env) package = Package( "demo", "0.1.0", source_type="legacy", source_reference="foo", source_url="https://legacy.foo.bar/simple/", ) package.files = [ { "file": filename, "hash": (f"sha256:{dist_hash_getter(filename).sha256}"), } for filename in [f"{package.name}-{package.version}.tar.gz"] ] res = chooser.choose_for(package) assert res.filename == "demo-0.1.0.tar.gz" ================================================ FILE: tests/installation/test_chooser_errors.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.core.packages.package import Package from poetry.installation.chooser import Chooser if TYPE_CHECKING: from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import MockEnv def test_chooser_no_links_found_error(env: MockEnv, pool: RepositoryPool) -> None: chooser = Chooser(pool, env) package = Package( "demo", "0.1.0", source_type="legacy", source_reference="foo", source_url="https://legacy.foo.bar/simple/", ) unsupported_wheels = {"demo-0.1.0-py3-none-any.whl"} error = chooser._no_links_found_error( package=package, links_seen=4, wheels_skipped=3, sdists_skipped=1, unsupported_wheels=unsupported_wheels, ) assert ( error.get_text(debug=True, strip=True) == f"""\ Unable to find installation candidates for {package.name} ({package.version}) This is likely not a Poetry issue. - 4 candidate(s) were identified for the package - 3 wheel(s) were skipped due to your installer.no-binary policy - 1 source distribution(s) were skipped due to your installer.only-binary policy - 1 wheel(s) were skipped as your project's environment does not support the identified abi tags The following wheel(s) were skipped as the current project environment does not support them due to abi compatibility \ issues. - {" -".join(unsupported_wheels)} If you would like to see the supported tags in your project environment, you can execute the following command: poetry debug tags Solutions: Make sure the lockfile is up-to-date. You can try one of the following; 1. Regenerate lockfile: poetry lock --no-cache --regenerate 2. Update package : poetry update --no-cache {package.name} If any of those solutions worked, you will have to clear your caches using (poetry cache clear --all). If neither works, please first check to verify that the {package.name} has published wheels available from your configured \ source ({package.source_reference}) that are compatible with your environment- ie. operating system, architecture \ (x86_64, arm64 etc.), python interpreter.\ """ ) assert ( error.get_text(debug=False, strip=True) == f"""\ Unable to find installation candidates for {package.name} ({package.version}) This is likely not a Poetry issue. - 4 candidate(s) were identified for the package - 3 wheel(s) were skipped due to your installer.no-binary policy - 1 source distribution(s) were skipped due to your installer.only-binary policy - 1 wheel(s) were skipped as your project's environment does not support the identified abi tags Solutions: Make sure the lockfile is up-to-date. You can try one of the following; 1. Regenerate lockfile: poetry lock --no-cache --regenerate 2. Update package : poetry update --no-cache {package.name} If any of those solutions worked, you will have to clear your caches using (poetry cache clear --all). If neither works, please first check to verify that the {package.name} has published wheels available from your configured \ source ({package.source_reference}) that are compatible with your environment- ie. operating system, architecture \ (x86_64, arm64 etc.), python interpreter. You can also run your poetry command with -v to see more information.\ """ ) ================================================ FILE: tests/installation/test_executor.py ================================================ from __future__ import annotations import csv import json import re import shutil import tempfile from pathlib import Path from subprocess import CalledProcessError from typing import TYPE_CHECKING from typing import Any import pytest from build import BuildBackendException from build import ProjectBuilder from cleo.formatters.style import Style from cleo.io.buffered_io import BufferedIO from cleo.io.outputs.output import Verbosity from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.utils.utils import path_to_url from poetry.factory import Factory from poetry.installation.chef import Chef as BaseChef from poetry.installation.executor import Executor from poetry.installation.operations import Install from poetry.installation.operations import Uninstall from poetry.installation.operations import Update from poetry.installation.wheel_installer import WheelInstaller from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.cache import ArtifactCache from poetry.utils.env import MockEnv from poetry.vcs.git.backend import Git if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterator from collections.abc import Mapping from collections.abc import Sequence from poetry.core.packages.package import PackageFile from pytest_mock import MockerFixture from poetry.config.config import Config from poetry.installation.operations.operation import Operation from poetry.repositories.pypi_repository import PyPiRepository from poetry.utils.env import VirtualEnv from tests.types import FixtureDirGetter class Chef(BaseChef): _directory_wheels: list[Path] | None = None _sdist_wheels: list[Path] | None = None _use_sdist = False def set_directory_wheel(self, wheels: Path | list[Path]) -> None: if not isinstance(wheels, list): wheels = [wheels] self._directory_wheels = wheels def set_sdist_wheel(self, wheels: Path | list[Path]) -> None: if not isinstance(wheels, list): wheels = [wheels] self._sdist_wheels = wheels def _prepare_sdist( self, archive: Path, destination: Path | None = None, config_settings: Mapping[str, str | Sequence[str]] | None = None, build_constraints: list[Dependency] | None = None, ) -> Path: if self._sdist_wheels is not None: self._use_sdist = True return super()._prepare_sdist( archive, destination, config_settings=config_settings, build_constraints=build_constraints, ) def _prepare( self, directory: Path, destination: Path, *, editable: bool = False, config_settings: Mapping[str, str | Sequence[str]] | None = None, build_constraints: list[Dependency] | None = None, ) -> Path: if self._use_sdist and self._sdist_wheels is not None: self._use_sdist = False wheel = self._sdist_wheels.pop(0) self._sdist_wheels.append(wheel) return wheel if self._directory_wheels is not None: wheel = self._directory_wheels.pop(0) self._directory_wheels.append(wheel) destination.mkdir(parents=True, exist_ok=True) dst_wheel = destination / wheel.name shutil.copyfile(wheel, dst_wheel) return dst_wheel return super()._prepare( directory, destination, editable=editable, config_settings=config_settings, build_constraints=build_constraints, ) @pytest.fixture def env(tmp_path: Path) -> MockEnv: path = tmp_path / ".venv" path.mkdir(parents=True) return MockEnv(path=path, is_venv=True) @pytest.fixture def io() -> BufferedIO: io = BufferedIO() io.output.formatter.set_style("c1_dark", Style("cyan", options=["dark"])) io.output.formatter.set_style("c2_dark", Style("default", options=["bold", "dark"])) io.output.formatter.set_style("success_dark", Style("green", options=["dark"])) io.output.formatter.set_style("warning", Style("yellow")) return io @pytest.fixture def io_decorated() -> BufferedIO: io = BufferedIO(decorated=True) io.output.formatter.set_style("c1", Style("cyan")) io.output.formatter.set_style("success", Style("green")) return io @pytest.fixture def io_not_decorated() -> BufferedIO: io = BufferedIO(decorated=False) return io @pytest.fixture def pool(pypi_repository: PyPiRepository) -> RepositoryPool: pool = RepositoryPool() pypi_repository._fallback = True pool.add_repository(pypi_repository) return pool @pytest.fixture def copy_wheel(tmp_path: Path, fixture_dir: FixtureDirGetter) -> Callable[[], Path]: def _copy_wheel() -> Path: tmp_name = tempfile.mktemp() (tmp_path / tmp_name).mkdir() shutil.copyfile( fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl", tmp_path / tmp_name / "demo-0.1.2-py2.py3-none-any.whl", ) return tmp_path / tmp_name / "demo-0.1.2-py2.py3-none-any.whl" return _copy_wheel @pytest.fixture def wheel(copy_wheel: Callable[[], Path]) -> Iterator[Path]: archive = copy_wheel() yield archive if archive.exists(): archive.unlink() def test_execute_executes_a_batch_of_operations( mocker: MockerFixture, config: Config, pool: RepositoryPool, io: BufferedIO, tmp_path: Path, env: MockEnv, copy_wheel: Callable[[], Path], fixture_dir: FixtureDirGetter, ) -> None: wheel_install = mocker.patch.object(WheelInstaller, "install") config.merge({"cache-dir": str(tmp_path)}) artifact_cache = ArtifactCache(cache_dir=config.artifacts_cache_directory) prepare_spy = mocker.spy(Chef, "_prepare") chef = Chef(artifact_cache, env, Factory.create_pool(config)) chef.set_directory_wheel([copy_wheel(), copy_wheel()]) chef.set_sdist_wheel(copy_wheel()) io.set_verbosity(Verbosity.VERY_VERBOSE) executor = Executor(env, pool, config, io) executor._chef = chef file_package = Package( "demo", "0.1.0", source_type="file", source_url=(fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl") .resolve() .as_posix(), ) directory_package = Package( "simple-project", "1.2.3", source_type="directory", source_url=fixture_dir("simple_project").resolve().as_posix(), ) git_package = Package( "demo", "0.1.0", source_type="git", source_reference="master", source_url="https://github.com/demo/demo.git", develop=True, ) return_code = executor.execute( [ Install(Package("pytest", "3.5.1")), Uninstall(Package("attrs", "17.4.0")), Update(Package("requests", "2.18.3"), Package("requests", "2.18.4")), Update(Package("pytest", "3.5.1"), Package("pytest", "3.5.0")), Uninstall(Package("clikit", "0.2.3")).skip("Not currently installed"), Install(file_package), Install(directory_package), Install(git_package), ] ) expected = f""" Package operations: 4 installs, 2 updates, 1 removal - Installing pytest (3.5.1) - Removing attrs (17.4.0) - Updating requests (2.18.3 -> 2.18.4) - Downgrading pytest (3.5.1 -> 3.5.0) - Installing demo (0.1.0 {file_package.source_url}) - Installing simple-project (1.2.3 {directory_package.source_url}) - Installing demo (0.1.0 master) """ expected_lines = set(expected.splitlines()) output_lines = set(io.fetch_output().splitlines()) assert output_lines == expected_lines assert wheel_install.call_count == 6 # 3 pip uninstalls: one for the remove operation and two for the update operations assert len(env.executed) == 3 assert return_code == 0 assert prepare_spy.call_count == 2 assert { args.args[1].name.split("-")[0]: args.kwargs.get("editable") for args in prepare_spy.call_args_list } == {"simple_project": False, "demo": True} @pytest.mark.parametrize("source_type", ["git", "file", "url"]) def test_execute_build_config_settings_passed( mocker: MockerFixture, config: Config, pool: RepositoryPool, io: BufferedIO, tmp_path: Path, env: MockEnv, copy_wheel: Callable[[], Path], fixture_dir: FixtureDirGetter, source_type: str, ) -> None: wheel_install = mocker.patch.object(WheelInstaller, "install") config_settings_demo = {"CC": "gcc", "--build-option": ["--one", "--two"]} config.merge( { "cache-dir": str(tmp_path), "installer": {"build-config-settings": {"demo": config_settings_demo}}, } ) artifact_cache = ArtifactCache(cache_dir=config.artifacts_cache_directory) prepare_spy = mocker.spy(Chef, "_prepare") chef = Chef(artifact_cache, env, Factory.create_pool(config)) chef.set_directory_wheel([copy_wheel(), copy_wheel()]) chef.set_sdist_wheel(copy_wheel()) executor = Executor(env, pool, config, io) executor._chef = chef directory_package = Package( "simple-project", "1.2.3", source_type="directory", source_url=fixture_dir("simple_project").resolve().as_posix(), ) if source_type == "git": ref = "master" demo_package = Package( "demo", "0.1.0", source_type="git", source_reference=ref, source_url="https://github.com/demo/demo.git", ) version_info = ref elif source_type == "file": url = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve().as_posix() demo_package = Package("demo", "0.1.0", source_type="file", source_url=url) version_info = url elif source_type == "url": url = "https://files.pythonhosted.org/demo-0.1.0.tar.gz" demo_package = Package("demo", "0.1.0", source_type="url", source_url=url) version_info = url else: raise ValueError return_code = executor.execute( [ Install(directory_package), Install(demo_package), ] ) expected = f""" Package operations: 2 installs, 0 updates, 0 removals - Installing simple-project (1.2.3 {directory_package.source_url}) - Installing demo (0.1.0 {version_info}) """ expected_lines = set(expected.splitlines()) output_lines = set(io.fetch_output().splitlines()) assert output_lines == expected_lines assert wheel_install.call_count == 2 assert return_code == 0 assert prepare_spy.call_count == 2 assert { args.args[1].name.split("-")[0]: args.kwargs.get("config_settings") for args in prepare_spy.call_args_list } == {"simple_project": None, "demo": config_settings_demo} @pytest.mark.parametrize("source_type", ["git", "file"]) def test_execute_build_constraints_passed( mocker: MockerFixture, config: Config, pool: RepositoryPool, io: BufferedIO, tmp_path: Path, env: MockEnv, copy_wheel: Callable[[], Path], fixture_dir: FixtureDirGetter, source_type: str, ) -> None: wheel_install = mocker.patch.object(WheelInstaller, "install") artifact_cache = ArtifactCache(cache_dir=config.artifacts_cache_directory) prepare_spy = mocker.spy(Chef, "_prepare") chef = Chef(artifact_cache, env, Factory.create_pool(config)) chef.set_directory_wheel([copy_wheel(), copy_wheel()]) chef.set_sdist_wheel(copy_wheel()) build_constraints_demo = [Dependency("setuptools", "<75")] build_constraints = {canonicalize_name("demo"): build_constraints_demo} executor = Executor(env, pool, config, io, build_constraints=build_constraints) executor._chef = chef directory_package = Package( "simple-project", "1.2.3", source_type="directory", source_url=fixture_dir("simple_project").resolve().as_posix(), ) if source_type == "git": ref = "master" demo_package = Package( "demo", "0.1.0", source_type="git", source_reference=ref, source_url="https://github.com/demo/demo.git", ) version_info = ref elif source_type == "file": url = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve().as_posix() demo_package = Package("demo", "0.1.0", source_type="file", source_url=url) version_info = url elif source_type == "url": url = "https://files.pythonhosted.org/demo-0.1.0.tar.gz" demo_package = Package("demo", "0.1.0", source_type="url", source_url=url) version_info = url else: raise ValueError return_code = executor.execute( [ Install(directory_package), Install(demo_package), ] ) expected = f""" Package operations: 2 installs, 0 updates, 0 removals - Installing simple-project (1.2.3 {directory_package.source_url}) - Installing demo (0.1.0 {version_info}) """ expected_lines = set(expected.splitlines()) output_lines = set(io.fetch_output().splitlines()) assert output_lines == expected_lines assert wheel_install.call_count == 2 assert return_code == 0 assert prepare_spy.call_count == 2 assert { args.args[1].name.split("-")[0]: args.kwargs.get("build_constraints") for args in prepare_spy.call_args_list } == {"simple_project": None, "demo": build_constraints_demo} @pytest.mark.parametrize( "operations, has_warning", [ ( [Install(Package("black", "21.11b0")), Install(Package("pytest", "3.5.1"))], True, ), ( [ Uninstall(Package("black", "21.11b0")), Uninstall(Package("pytest", "3.5.1")), ], False, ), ( [ Update(Package("black", "19.10b0"), Package("black", "21.11b0")), Update(Package("pytest", "3.5.0"), Package("pytest", "3.5.1")), ], True, ), ], ) def test_execute_prints_warning_for_yanked_package( config: Config, pool: RepositoryPool, io: BufferedIO, tmp_path: Path, env: MockEnv, operations: list[Operation], has_warning: bool, ) -> None: config.merge({"cache-dir": str(tmp_path)}) executor = Executor(env, pool, config, io) return_code = executor.execute(operations) expected = ( "Warning: The file chosen for install of black 21.11b0 " "(black-21.11b0-py3-none-any.whl) is yanked. Reason for being yanked: " "Broken regex dependency. Use 21.11b1 instead." ) output = io.fetch_output() error = io.fetch_error() assert return_code == 0, f"\noutput: {output}\nerror: {error}\n" assert "pytest" not in error if has_warning: assert expected in error assert error.count("is yanked") == 1 else: assert expected not in error assert error.count("yanked") == 0 @pytest.mark.skip(reason="https://github.com/python-poetry/poetry/issues/7983") def test_execute_prints_warning_for_invalid_wheels( config: Config, pool: RepositoryPool, io: BufferedIO, tmp_path: Path, env: MockEnv, ) -> None: config.merge({"cache-dir": str(tmp_path)}) executor = Executor(env, pool, config, io) base_url = "https://files.pythonhosted.org/" wheel1 = "demo_invalid_record-0.1.0-py2.py3-none-any.whl" wheel2 = "demo_invalid_record2-0.1.0-py2.py3-none-any.whl" return_code = executor.execute( [ Install( Package( "demo-invalid-record", "0.1.0", source_type="url", source_url=f"{base_url}/{wheel1}", ) ), Install( Package( "demo-invalid-record2", "0.1.0", source_type="url", source_url=f"{base_url}/{wheel2}", ) ), ] ) warning1 = f"""\ Warning: Validation of the RECORD file of {wheel1} failed.\ Please report to the maintainers of that package so they can fix their build process.\ Details: In .*?{wheel1}, demo/__init__.py is not mentioned in RECORD In .*?{wheel1}, demo_invalid_record-0.1.0.dist-info/WHEEL is not mentioned in RECORD """ warning2 = f"""\ Warning: Validation of the RECORD file of {wheel2} failed.\ Please report to the maintainers of that package so they can fix their build process.\ Details: In .*?{wheel2}, hash / size of demo_invalid_record2-0.1.0.dist-info/METADATA didn't\ match RECORD """ output = io.fetch_output() error = io.fetch_error() assert return_code == 0, f"\noutput: {output}\nerror: {error}\n" assert re.match(f"{warning1}\n{warning2}", error) or re.match( f"{warning2}\n{warning1}", error ), error def test_execute_shows_skipped_operations_if_verbose( config: Config, pool: RepositoryPool, io: BufferedIO, config_cache_dir: Path, env: MockEnv, ) -> None: config.merge({"cache-dir": config_cache_dir.as_posix()}) executor = Executor(env, pool, config, io) executor.verbose() assert ( executor.execute( [Uninstall(Package("clikit", "0.2.3")).skip("Not currently installed")] ) == 0 ) expected = """ Package operations: 0 installs, 0 updates, 0 removals, 1 skipped - Removing clikit (0.2.3): Skipped for the following reason: Not currently installed """ assert io.fetch_output() == expected assert len(env.executed) == 0 def test_execute_should_show_errors( config: Config, pool: RepositoryPool, mocker: MockerFixture, io: BufferedIO, env: MockEnv, ) -> None: executor = Executor(env, pool, config, io) executor.verbose() mocker.patch.object(executor, "_install", side_effect=Exception("It failed!")) assert executor.execute([Install(Package("clikit", "0.2.3"))]) == 1 expected = """ Package operations: 1 install, 0 updates, 0 removals - Installing clikit (0.2.3) Exception It failed! """ assert expected in io.fetch_output() def test_execute_works_with_ansi_output( config: Config, pool: RepositoryPool, io_decorated: BufferedIO, tmp_path: Path, env: MockEnv, ) -> None: config.merge({"cache-dir": str(tmp_path)}) executor = Executor(env, pool, config, io_decorated) return_code = executor.execute( [ Install(Package("cleo", "1.0.0a5")), ] ) # fmt: off expected = [ "\x1b[39;1mPackage operations\x1b[39;22m: \x1b[34m1\x1b[39m install, \x1b[34m0\x1b[39m updates, \x1b[34m0\x1b[39m removals", "\x1b[34;1m-\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m", "\x1b[34;1m-\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m", "\x1b[34;1m-\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m", "\x1b[32;1m-\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[32m1.0.0a5\x1b[39m\x1b[39m)\x1b[39m", # finished ] # fmt: on output = io_decorated.fetch_output() # hint: use print(repr(output)) if you need to debug this for line in expected: assert line in output assert return_code == 0 def test_execute_works_with_no_ansi_output( mocker: MockerFixture, config: Config, pool: RepositoryPool, io_not_decorated: BufferedIO, tmp_path: Path, env: MockEnv, ) -> None: config.merge({"cache-dir": str(tmp_path)}) executor = Executor(env, pool, config, io_not_decorated) return_code = executor.execute( [ Install(Package("cleo", "1.0.0a5")), ] ) expected = """ Package operations: 1 install, 0 updates, 0 removals - Installing cleo (1.0.0a5) """ expected_lines = set(expected.splitlines()) output_lines = set(io_not_decorated.fetch_output().splitlines()) assert output_lines == expected_lines assert return_code == 0 def test_execute_should_show_operation_as_cancelled_on_subprocess_keyboard_interrupt( config: Config, pool: RepositoryPool, mocker: MockerFixture, io: BufferedIO, env: MockEnv, ) -> None: executor = Executor(env, pool, config, io) executor.verbose() # A return code of -2 means KeyboardInterrupt in the pip subprocess mocker.patch.object(executor, "_install", return_value=-2) assert executor.execute([Install(Package("clikit", "0.2.3"))]) == 1 expected = """ Package operations: 1 install, 0 updates, 0 removals - Installing clikit (0.2.3) - Installing clikit (0.2.3): Cancelled """ assert io.fetch_output() == expected def test_execute_should_gracefully_handle_io_error( config: Config, pool: RepositoryPool, mocker: MockerFixture, io: BufferedIO, env: MockEnv, ) -> None: executor = Executor(env, pool, config, io) executor.verbose() original_write_line = executor._io.write_line def write_line(string: str, **kwargs: Any) -> None: # Simulate UnicodeEncodeError string = string.replace("-", "•") string.encode("ascii") original_write_line(string, **kwargs) mocker.patch.object(io, "write_line", side_effect=write_line) assert executor.execute([Install(Package("clikit", "0.2.3"))]) == 1 expected = r""" Package operations: 1 install, 0 updates, 0 removals \s*Unicode\w+Error """ assert re.match(expected, io.fetch_output()) def test_executor_should_delete_incomplete_downloads( config: Config, io: BufferedIO, tmp_path: Path, mocker: MockerFixture, pool: RepositoryPool, env: MockEnv, ) -> None: cached_archive = tmp_path / "tomlkit-0.5.3-py2.py3-none-any.whl" def download_fail(*_: Any) -> None: cached_archive.touch() # broken archive raise Exception("Download error") mocker.patch( "poetry.installation.executor.Executor._download_archive", side_effect=download_fail, ) mocker.patch( "poetry.utils.cache.ArtifactCache._get_cached_archive", return_value=None, ) mocker.patch( "poetry.utils.cache.ArtifactCache.get_cache_directory_for_link", return_value=tmp_path, ) config.merge({"cache-dir": str(tmp_path)}) executor = Executor(env, pool, config, io) with pytest.raises(Exception, match="Download error"): executor._download(Install(Package("tomlkit", "0.5.3"))) assert not cached_archive.exists() def verify_installed_distribution( venv: VirtualEnv, package: Package, url_reference: dict[str, Any] | None = None ) -> None: distributions = list(venv.site_packages.distributions(name=package.name)) assert len(distributions) == 1 distribution = distributions[0] metadata = distribution.metadata assert metadata assert metadata["Name"] == package.name assert metadata["Version"] == package.version.text direct_url_file = distribution._path.joinpath( # type: ignore[attr-defined] "direct_url.json" ) if url_reference is not None: record_file = distribution._path.joinpath( # type: ignore[attr-defined] "RECORD" ) with open(record_file, encoding="utf-8", newline="") as f: reader = csv.reader(f) rows = list(reader) assert all(len(row) == 3 for row in rows) record_entries = {row[0] for row in rows} direct_url_entry = direct_url_file.relative_to(record_file.parent.parent) assert direct_url_file.exists() assert str(direct_url_entry) in record_entries assert json.loads(direct_url_file.read_text(encoding="utf-8")) == url_reference else: assert not direct_url_file.exists() @pytest.mark.parametrize( "package", [ Package("demo", "0.1.0"), # PyPI Package( # private source "demo", "0.1.0", source_type="legacy", source_url="http://localhost:3141/root/pypi/+simple", source_reference="private", ), ], ) def test_executor_should_not_write_pep610_url_references_for_cached_package( package: Package, mocker: MockerFixture, fixture_dir: FixtureDirGetter, tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO, ) -> None: link_cached = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" package.files = [ { "file": "demo-0.1.0-py2.py3-none-any.whl", "hash": ( "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" ), } ] mocker.patch( "poetry.installation.executor.Executor._download", return_value=link_cached ) executor = Executor(tmp_venv, pool, config, io) executor.execute([Install(package)]) verify_installed_distribution(tmp_venv, package) assert link_cached.exists(), "cached file should not be deleted" def test_executor_should_write_pep610_url_references_for_wheel_files( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO, fixture_dir: FixtureDirGetter, ) -> None: url = (fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl").resolve() package = Package("demo", "0.1.0", source_type="file", source_url=url.as_posix()) # Set package.files so the executor will attempt to hash the package package.files = [ { "file": "demo-0.1.0-py2.py3-none-any.whl", "hash": ( "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" ), } ] executor = Executor(tmp_venv, pool, config, io) executor.execute([Install(package)]) expected_url_reference = { "archive_info": { "hashes": { "sha256": ( "70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" ) }, }, "url": url.as_uri(), } verify_installed_distribution(tmp_venv, package, expected_url_reference) assert url.exists(), "source file should not be deleted" def test_executor_should_write_pep610_url_references_for_non_wheel_files( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO, fixture_dir: FixtureDirGetter, ) -> None: url = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve() package = Package("demo", "0.1.0", source_type="file", source_url=url.as_posix()) # Set package.files so the executor will attempt to hash the package package.files = [ { "file": "demo-0.1.0.tar.gz", "hash": ( "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" ), } ] executor = Executor(tmp_venv, pool, config, io) executor.execute([Install(package)]) expected_url_reference = { "archive_info": { "hashes": { "sha256": ( "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" ) }, }, "url": url.as_uri(), } verify_installed_distribution(tmp_venv, package, expected_url_reference) assert url.exists(), "source file should not be deleted" def test_executor_should_write_pep610_url_references_for_directories( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, fixture_dir: FixtureDirGetter, mocker: MockerFixture, ) -> None: url = (fixture_dir("git") / "github.com" / "demo" / "demo").resolve() package = Package( "demo", "0.1.2", source_type="directory", source_url=url.as_posix() ) chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) prepare_spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef executor.execute([Install(package)]) verify_installed_distribution( tmp_venv, package, {"dir_info": {}, "url": url.as_uri()} ) assert not prepare_spy.spy_return.exists(), "archive not cleaned up" def test_executor_should_write_pep610_url_references_for_editable_directories( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, fixture_dir: FixtureDirGetter, mocker: MockerFixture, ) -> None: url = (fixture_dir("git") / "github.com" / "demo" / "demo").resolve() package = Package( "demo", "0.1.2", source_type="directory", source_url=url.as_posix(), develop=True, ) chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) prepare_spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef executor.execute([Install(package)]) verify_installed_distribution( tmp_venv, package, {"dir_info": {"editable": True}, "url": url.as_uri()} ) assert not prepare_spy.spy_return.exists(), "archive not cleaned up" @pytest.mark.parametrize("is_artifact_cached", [False, True]) def test_executor_should_write_pep610_url_references_for_wheel_urls( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO, mocker: MockerFixture, fixture_dir: FixtureDirGetter, is_artifact_cached: bool, ) -> None: if is_artifact_cached: link_cached = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" mocker.patch( "poetry.utils.cache.ArtifactCache.get_cached_archive_for_link", return_value=link_cached, ) download_spy = mocker.spy(Executor, "_download_archive") package = Package( "demo", "0.1.0", source_type="url", source_url="https://files.pythonhosted.org/demo-0.1.0-py2.py3-none-any.whl", ) # Set package.files so the executor will attempt to hash the package package.files = [ { "file": "demo-0.1.0-py2.py3-none-any.whl", "hash": ( "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" ), } ] executor = Executor(tmp_venv, pool, config, io) operation = Install(package) executor.execute([operation]) expected_url_reference = { "archive_info": { "hashes": { "sha256": ( "70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" ) }, }, "url": package.source_url, } verify_installed_distribution(tmp_venv, package, expected_url_reference) if is_artifact_cached: download_spy.assert_not_called() else: assert package.source_url is not None download_spy.assert_called_once_with( mocker.ANY, operation, package.source_url, dest=mocker.ANY, ) dest = download_spy.call_args.args[3] assert dest.exists(), "cached file should not be deleted" @pytest.mark.parametrize( ( "is_sdist_cached", "is_wheel_cached", "expect_artifact_building", "expect_artifact_download", ), [ (True, False, True, False), (True, True, False, False), (False, False, True, True), (False, True, False, True), ], ) def test_executor_should_write_pep610_url_references_for_non_wheel_urls( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO, mocker: MockerFixture, fixture_dir: FixtureDirGetter, is_sdist_cached: bool, is_wheel_cached: bool, expect_artifact_building: bool, expect_artifact_download: bool, ) -> None: built_wheel = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" mock_prepare = mocker.patch( "poetry.installation.chef.Chef._prepare", return_value=built_wheel, ) download_spy = mocker.spy(Executor, "_download_archive") if is_sdist_cached or is_wheel_cached: cached_sdist = fixture_dir("distributions") / "demo-0.1.0.tar.gz" cached_wheel = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" def mock_get_cached_archive_func( _cache_dir: Path, *, strict: bool, **__: Any ) -> Path | None: if is_wheel_cached and not strict: return cached_wheel if is_sdist_cached: return cached_sdist return None mocker.patch( "poetry.utils.cache.ArtifactCache._get_cached_archive", side_effect=mock_get_cached_archive_func, ) package = Package( "demo", "0.1.0", source_type="url", source_url="https://files.pythonhosted.org/demo-0.1.0.tar.gz", ) # Set package.files so the executor will attempt to hash the package package.files = [ { "file": "demo-0.1.0.tar.gz", "hash": ( "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" ), } ] executor = Executor(tmp_venv, pool, config, io) operation = Install(package) executor.execute([operation]) expected_url_reference = { "archive_info": { "hashes": { "sha256": ( "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" ) }, }, "url": package.source_url, } verify_installed_distribution(tmp_venv, package, expected_url_reference) if expect_artifact_building: mock_prepare.assert_called_once() else: mock_prepare.assert_not_called() if expect_artifact_download: assert package.source_url is not None download_spy.assert_called_once_with( mocker.ANY, operation, package.source_url, dest=mocker.ANY ) dest = download_spy.call_args.args[3] assert dest.exists(), "cached file should not be deleted" else: download_spy.assert_not_called() @pytest.mark.parametrize( "source_url,written_source_url", [ ("https://github.com/demo/demo.git", "https://github.com/demo/demo.git"), ("git@github.com:demo/demo.git", "ssh://git@github.com/demo/demo.git"), ], ) @pytest.mark.parametrize("is_artifact_cached", [False, True]) def test_executor_should_write_pep610_url_references_for_git( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, mocker: MockerFixture, fixture_dir: FixtureDirGetter, source_url: str, written_source_url: str, is_artifact_cached: bool, ) -> None: if is_artifact_cached: link_cached = fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl" mocker.patch( "poetry.utils.cache.ArtifactCache.get_cached_archive_for_git", return_value=link_cached, ) clone_spy = mocker.spy(Git, "clone") source_resolved_reference = "123456" source_url = source_url package = Package( "demo", "0.1.2", source_type="git", source_reference="master", source_resolved_reference=source_resolved_reference, source_url=source_url, ) assert package.source_url == written_source_url chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) prepare_spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef executor.execute([Install(package)]) verify_installed_distribution( tmp_venv, package, { "vcs_info": { "vcs": "git", "requested_revision": "master", "commit_id": "123456", }, "url": package.source_url, }, ) if is_artifact_cached: clone_spy.assert_not_called() prepare_spy.assert_not_called() else: clone_spy.assert_called_once_with( url=package.source_url, source_root=mocker.ANY, revision=source_resolved_reference, ) prepare_spy.assert_called_once() assert prepare_spy.spy_return.exists(), "cached file should not be deleted" assert (prepare_spy.spy_return.parent / ".created_from_git_dependency").exists() def test_executor_should_write_pep610_url_references_for_editable_git( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, mocker: MockerFixture, fixture_dir: FixtureDirGetter, ) -> None: source_resolved_reference = "123456" source_url = "https://github.com/demo/demo.git" package = Package( "demo", "0.1.2", source_type="git", source_reference="master", source_resolved_reference=source_resolved_reference, source_url=source_url, develop=True, ) chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) prepare_spy = mocker.spy(chef, "prepare") cache_spy = mocker.spy(artifact_cache, "get_cached_archive_for_git") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef executor.execute([Install(package)]) assert package.source_url is not None verify_installed_distribution( tmp_venv, package, { "dir_info": {"editable": True}, "url": Path(package.source_url).as_uri(), }, ) cache_spy.assert_not_called() prepare_spy.assert_called_once() assert not prepare_spy.spy_return.exists(), "editable git should not be cached" assert not (prepare_spy.spy_return.parent / ".created_from_git_dependency").exists() def test_executor_should_append_subdirectory_for_git( mocker: MockerFixture, tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, ) -> None: package = Package( "demo", "0.1.2", source_type="git", source_reference="master", source_resolved_reference="123456", source_url="https://github.com/demo/subdirectories.git", source_subdirectory="two", ) chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef executor.execute([Install(package)]) archive_arg = spy.call_args[0][0] assert archive_arg == tmp_venv.path / "src/subdirectories/two" def test_executor_should_install_multiple_packages_from_same_git_repository( mocker: MockerFixture, tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, ) -> None: package_a = Package( "package_a", "0.1.2", source_type="git", source_reference="master", source_resolved_reference="123456", source_url="https://github.com/demo/subdirectories.git", source_subdirectory="package_a", ) package_b = Package( "package_b", "0.1.2", source_type="git", source_reference="master", source_resolved_reference="123456", source_url="https://github.com/demo/subdirectories.git", source_subdirectory="package_b", ) chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef executor.execute([Install(package_a), Install(package_b)]) archive_arg = spy.call_args_list[0][0][0] assert archive_arg == tmp_venv.path / "src/subdirectories/package_a" archive_arg = spy.call_args_list[1][0][0] assert archive_arg == tmp_venv.path / "src/subdirectories/package_b" def test_executor_should_install_multiple_packages_from_forked_git_repository( mocker: MockerFixture, tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, ) -> None: package_a = Package( "one", "1.0.0", source_type="git", source_reference="master", source_resolved_reference="123456", source_url="https://github.com/demo/subdirectories.git", source_subdirectory="one", ) package_b = Package( "two", "2.0.0", source_type="git", source_reference="master", source_resolved_reference="123456", source_url="https://github.com/forked_demo/subdirectories.git", source_subdirectory="two", ) chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) prepare_spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef executor.execute([Install(package_a), Install(package_b)]) # Verify that the repo for package_a is not re-used for package_b. # both repos must be cloned serially into separate directories. # If so, executor.prepare() will be called twice. assert prepare_spy.call_count == 2 def test_executor_should_write_pep610_url_references_for_git_with_subdirectories( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, ) -> None: package = Package( "demo", "0.1.2", source_type="git", source_reference="master", source_resolved_reference="123456", source_url="https://github.com/demo/subdirectories.git", source_subdirectory="two", ) chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) executor = Executor(tmp_venv, pool, config, io) executor._chef = chef executor.execute([Install(package)]) verify_installed_distribution( tmp_venv, package, { "vcs_info": { "vcs": "git", "requested_revision": "master", "commit_id": "123456", }, "url": package.source_url, "subdirectory": package.source_subdirectory, }, ) @pytest.mark.parametrize( ("max_workers", "cpu_count", "side_effect", "expected_workers"), [ (None, 3, None, 7), (3, 4, None, 3), (8, 3, None, 7), (None, 8, NotImplementedError(), 5), (2, 8, NotImplementedError(), 2), (8, 8, NotImplementedError(), 5), ], ) def test_executor_should_be_initialized_with_correct_workers( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO, mocker: MockerFixture, max_workers: int | None, cpu_count: int | None, side_effect: Exception | None, expected_workers: int, ) -> None: config.merge({"installer": {"max-workers": max_workers}}) mocker.patch("os.cpu_count", return_value=cpu_count, side_effect=side_effect) executor = Executor(tmp_venv, pool, config, io) assert executor._max_workers == expected_workers @pytest.mark.parametrize("failing_method", ["build", "get_requires_for_build"]) @pytest.mark.parametrize( "exception", [ CalledProcessError(1, ["pip"], output=b"original error"), Exception("original error"), ], ) @pytest.mark.parametrize("editable", [False, True]) @pytest.mark.parametrize("source_type", ["directory", "git", "git subdirectory"]) def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( failing_method: str, exception: Exception, editable: bool, source_type: str, mocker: MockerFixture, config: Config, pool: RepositoryPool, io: BufferedIO, env: MockEnv, fixture_dir: FixtureDirGetter, ) -> None: error = BuildBackendException(exception, description="hide the original error") mocker.patch.object(ProjectBuilder, failing_method, side_effect=error) io.set_verbosity(Verbosity.NORMAL) executor = Executor(env, pool, config, io) package_name = "simple-project" package_version = "1.2.3" source_reference: str | None = None source_sub_directory: str | None = None if source_type == "directory": source_url = fixture_dir("simple_project").resolve().as_posix() source_resolved_reference = None pip_url = path_to_url(source_url) pip_editable_requirement = source_url elif source_type == "git": source_url = "https://github.com/demo/demo.git" source_reference = "v2.0" source_resolved_reference = "12345678" pip_url = f"git+{source_url}@{source_reference}" pip_editable_requirement = f"{pip_url}#egg={package_name}" elif source_type == "git subdirectory": source_type = "git" source_sub_directory = "one" source_url = "https://github.com/demo/subdirectories.git" source_reference = "v2.0" source_resolved_reference = "12345678" pip_base_url = f"git+{source_url}@{source_reference}" pip_url = f"{pip_base_url}#subdirectory={source_sub_directory}" pip_editable_requirement = ( f"{pip_base_url}#egg={package_name}&subdirectory={source_sub_directory}" ) else: raise ValueError(f"Unknown source type: {source_type}") package = Package( package_name, package_version, source_type=source_type, source_url=source_url, source_reference=source_reference, source_resolved_reference=source_resolved_reference, source_subdirectory=source_sub_directory, develop=editable, ) # must not be included in the error message package.python_versions = ">=3.7" return_code = executor.execute([Install(package)]) assert return_code == 1 assert package.source_url is not None if editable: pip_command = "pip wheel --no-cache-dir --use-pep517 --editable" requirement = pip_editable_requirement if source_type == "directory": assert Path(requirement).exists() else: pip_command = "pip wheel --no-cache-dir --use-pep517" requirement = f"{package_name} @ {pip_url}" version_details = package.source_resolved_reference or package.source_url expected_source_string = f"{package_name} ({package_version} {version_details})" expected_pip_command = f'{pip_command} "{requirement}"' expected_output = f""" Package operations: 1 install, 0 updates, 0 removals - Installing {expected_source_string} PEP517 build of a dependency failed hide the original error """ if isinstance(exception, CalledProcessError): expected_output += ( "\n | Command '['pip']' returned non-zero exit status 1." "\n | " "\n | original error" "\n" ) expected_output += f""" Note: This error originates from the build backend, and is likely not a problem \ with poetry but one of the following issues with {expected_source_string} - not supporting PEP 517 builds - not specifying PEP 517 build requirements correctly - the build requirements are incompatible with your operating system or Python version - the build requirements are missing system dependencies (eg: compilers, libraries, headers). You can verify this by running {expected_pip_command}. """ assert io.fetch_output() == expected_output @pytest.mark.parametrize("encoding", ["utf-8", "latin-1"]) @pytest.mark.parametrize("stderr", [None, "Errör on stderr"]) def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess_encoding( encoding: str, stderr: str | None, mocker: MockerFixture, config: Config, pool: RepositoryPool, io: BufferedIO, env: MockEnv, fixture_dir: FixtureDirGetter, ) -> None: """Test that the output of the subprocess is decoded correctly.""" stdout = "Errör on stdout" error = BuildBackendException( CalledProcessError( 1, ["pip"], output=stdout.encode(encoding), stderr=stderr.encode(encoding) if stderr else None, ) ) mocker.patch.object(ProjectBuilder, "get_requires_for_build", side_effect=error) io.set_verbosity(Verbosity.NORMAL) executor = Executor(env, pool, config, io) directory_package = Package( "simple-project", "1.2.3", source_type="directory", source_url=fixture_dir("simple_project").resolve().as_posix(), ) return_code = executor.execute([Install(directory_package)]) assert return_code == 1 assert (stderr or stdout) in io.fetch_output() def test_build_system_requires_not_available( config: Config, pool: RepositoryPool, io: BufferedIO, env: MockEnv, fixture_dir: FixtureDirGetter, ) -> None: io.set_verbosity(Verbosity.NORMAL) executor = Executor(env, pool, config, io) package_name = "simple-project" package_version = "1.2.3" directory_package = Package( package_name, package_version, source_type="directory", source_url=fixture_dir("build_system_requires_not_available") .resolve() .as_posix(), ) return_code = executor.execute([Install(directory_package)]) assert return_code == 1 package_url = directory_package.source_url expected_start = f"""\ Package operations: 1 install, 0 updates, 0 removals - Installing {package_name} ({package_version} {package_url}) SolveFailureError Because -root- depends on poetry-core (0.999) which doesn't match any versions,\ version solving failed. """ expected_end = "Cannot resolve build-system.requires for simple-project." output = io.fetch_output().strip() assert output.startswith(expected_start) assert output.endswith(expected_end) def test_build_system_requires_install_failure( mocker: MockerFixture, config: Config, pool: RepositoryPool, io: BufferedIO, env: MockEnv, fixture_dir: FixtureDirGetter, ) -> None: mocker.patch("poetry.installation.installer.Installer.run", return_value=1) mocker.patch("cleo.io.buffered_io.BufferedIO.fetch_output", return_value="output") mocker.patch("cleo.io.buffered_io.BufferedIO.fetch_error", return_value="error") io.set_verbosity(Verbosity.NORMAL) executor = Executor(env, pool, config, io) package_name = "simple-project" package_version = "1.2.3" directory_package = Package( package_name, package_version, source_type="directory", source_url=fixture_dir("simple_project").resolve().as_posix(), ) return_code = executor.execute([Install(directory_package)]) assert return_code == 1 package_url = directory_package.source_url expected_start = f"""\ Package operations: 1 install, 0 updates, 0 removals - Installing {package_name} ({package_version} {package_url}) IsolatedBuildInstallError Failed to install poetry-core>=1.1.0a7. \ Output: output \ Error: error """ expected_end = "Cannot install build-system.requires for simple-project." mocker.stopall() # to get real output output = io.fetch_output().strip() assert output.startswith(expected_start) assert output.endswith(expected_end) def test_other_error( config: Config, pool: RepositoryPool, io: BufferedIO, env: MockEnv, fixture_dir: FixtureDirGetter, ) -> None: io.set_verbosity(Verbosity.NORMAL) executor = Executor(env, pool, config, io) package_name = "simple-project" package_version = "1.2.3" directory_package = Package( package_name, package_version, source_type="directory", source_url=fixture_dir("non-existing").resolve().as_posix(), ) return_code = executor.execute([Install(directory_package)]) assert return_code == 1 package_url = directory_package.source_url expected_start = f"""\ Package operations: 1 install, 0 updates, 0 removals - Installing {package_name} ({package_version} {package_url}) FileNotFoundError """ expected_end = "Cannot install simple-project." output = io.fetch_output().strip() assert output.startswith(expected_start) assert output.endswith(expected_end) @pytest.mark.parametrize( "package_files,expected_url_reference", [ ( [ { "file": "demo-0.1.0.tar.gz", "hash": "sha512:766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad", }, { "file": "demo-0.1.0.tar.gz", "hash": "md5:d1912c917363a64e127318655f7d1fe7", }, { "file": "demo-0.1.0.whl", "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", }, ], { "archive_info": { "hashes": { "sha512": "766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad" }, }, }, ), ( [ { "file": "demo-0.1.0.tar.gz", "hash": "md5:d1912c917363a64e127318655f7d1fe7", } ], { "archive_info": { "hashes": {"md5": "d1912c917363a64e127318655f7d1fe7"}, }, }, ), ( [ { "file": "demo-0.1.0.tar.gz", "hash": "sha3_512:196f4af9099185054ed72ca1d4c57707da5d724df0af7c3dfcc0fd018b0e0533908e790a291600c7d196fe4411b4f5f6db45213fe6e5cd5512bf18b2e9eff728", }, { "file": "demo-0.1.0.tar.gz", "hash": "sha512:766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad", }, { "file": "demo-0.1.0.tar.gz", "hash": "md5:d1912c917363a64e127318655f7d1fe7", }, { "file": "demo-0.1.0.whl", "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", }, ], { "archive_info": { "hashes": { "sha3_512": "196f4af9099185054ed72ca1d4c57707da5d724df0af7c3dfcc0fd018b0e0533908e790a291600c7d196fe4411b4f5f6db45213fe6e5cd5512bf18b2e9eff728" }, }, }, ), ], ) def test_executor_known_hashes( package_files: list[PackageFile], expected_url_reference: dict[str, Any], tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO, fixture_dir: FixtureDirGetter, ) -> None: package_source_url: Path = ( fixture_dir("distributions") / "demo-0.1.0.tar.gz" ).resolve() package = Package( "demo", "0.1.0", source_type="file", source_url=package_source_url.as_posix() ) package.files = package_files executor = Executor(tmp_venv, pool, config, io) executor.execute([Install(package)]) expected_url_reference["url"] = package_source_url.as_uri() verify_installed_distribution(tmp_venv, package, expected_url_reference) def test_executor_no_supported_hash_types( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO, fixture_dir: FixtureDirGetter, ) -> None: url = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve() package = Package("demo", "0.1.0", source_type="file", source_url=url.as_posix()) # Set package.files so the executor will attempt to hash the package package.files = [ { "file": "demo-0.1.0.tar.gz", "hash": "hash_blah:1234567890abcdefghijklmnopqrstyzwxyz", }, { "file": "demo-0.1.0.whl", "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", }, ] executor = Executor(tmp_venv, pool, config, io) return_code = executor.execute([Install(package)]) distributions = list(tmp_venv.site_packages.distributions(name=package.name)) assert len(distributions) == 0 output = io.fetch_output() error = io.fetch_error() assert return_code == 1, f"\noutput: {output}\nerror: {error}\n" assert "No usable hash type(s) for demo" in output assert "hash_blah:1234567890abcdefghijklmnopqrstyzwxyz" in output ================================================ FILE: tests/installation/test_installer.py ================================================ from __future__ import annotations import json import re import shutil from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import cast import pytest from cleo.io.buffered_io import BufferedIO from cleo.io.inputs.input import Input from cleo.io.null_io import NullIO from cleo.io.outputs.output import Verbosity from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.dependency_group import DependencyGroup from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.factory import Factory from poetry.installation import Installer from poetry.packages import Locker as BaseLocker from poetry.repositories import Repository from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository from poetry.toml.file import TOMLFile from poetry.utils.constants import POETRY_SYSTEM_PROJECT_NAME from poetry.utils.env import MockEnv from poetry.utils.env import NullEnv from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import TestExecutor from tests.helpers import get_dependency from tests.helpers import get_package if TYPE_CHECKING: from collections.abc import Iterator from _pytest.fixtures import FixtureRequest from pytest_mock import MockerFixture from tomlkit import TOMLDocument from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pypi_repository import PyPiRepository from poetry.utils.env import Env from tests.conftest import Config from tests.types import FixtureDirGetter from tests.types import PackageFactory class CustomInstalledRepository(InstalledRepository): @classmethod def load( cls, env: Env, with_dependencies: bool = False ) -> CustomInstalledRepository: return cls() class Locker(BaseLocker): def __init__(self, lock_path: Path) -> None: self._lock = lock_path / "poetry.lock" self._written_data = None self._locked = False self._fresh = True self._lock_data = None self._content_hash = self._get_content_hash() @property def written_data(self) -> dict[str, Any]: assert self._written_data is not None return self._written_data def set_lock_path(self, lock: Path) -> Locker: self._lock = lock / "poetry.lock" return self def locked(self, is_locked: bool = True) -> Locker: self._locked = is_locked return self def mock_lock_data(self, data: dict[str, Any]) -> None: self._lock_data = data def is_locked(self) -> bool: return self._locked def fresh(self, is_fresh: bool = True) -> Locker: self._fresh = is_fresh return self def is_fresh(self) -> bool: return self._fresh def _get_content_hash(self, *, with_dependency_groups: bool = True) -> str: return "123456789" def _write_lock_data(self, data: dict[str, Any]) -> None: for package in data["package"]: python_versions = str(package["python-versions"]) package["python-versions"] = python_versions self._written_data = json.loads(json.dumps(data)) self._lock_data = data @pytest.fixture(autouse=True, params=[False, True]) def config_installer_reresolve( config: Config, request: FixtureRequest ) -> Iterator[bool]: config.config["installer"]["re-resolve"] = request.param yield request.param @pytest.fixture() def package() -> ProjectPackage: p = ProjectPackage("root", "1.0") p.root_dir = Path.cwd() return p @pytest.fixture() def repo() -> Repository: return Repository("repo") @pytest.fixture() def pool(repo: Repository) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(repo) return pool @pytest.fixture() def installed() -> CustomInstalledRepository: return CustomInstalledRepository() @pytest.fixture() def locker(project_root: Path) -> Locker: return Locker(lock_path=project_root) @pytest.fixture() def env(tmp_path: Path) -> NullEnv: return NullEnv(path=tmp_path) @pytest.fixture() def installer( package: ProjectPackage, pool: RepositoryPool, locker: Locker, env: NullEnv, installed: CustomInstalledRepository, config: Config, ) -> Installer: return Installer( NullIO(), env, package, locker, pool, config, installed=installed, executor=TestExecutor(env, pool, config, NullIO()), ) def fixture(name: str, data: dict[str, Any] | None = None) -> dict[str, Any]: """ Create or load a fixture file in TOML format. This function retrieves the contents of a test fixture file, optionally writing data to it before reading, and returns the data as a dictionary. It is used to manage testing fixtures for TOML-based configurations. :param name: Name of the fixture file (without extension, default of .test is appended). :param data: Dictionary to write to the file as a TOML document. If None, no data is written (use this only when generating fixtures). :return: Dictionary representing the contents of the TOML fixture file. """ file = TOMLFile(Path(__file__).parent / "fixtures" / f"{name}.test") if data: # if data is provided write it, this is helpful for generating fixtures # we expect lock data to be compatible with TOMLDocument for our purposes file.write(cast("TOMLDocument", data)) content: dict[str, Any] = file.read() return content def fix_lock_data(lock_data: dict[str, Any]) -> None: if Version.parse(lock_data["metadata"]["lock-version"]) >= Version.parse("2.1"): for locked_package in lock_data["package"]: locked_package["groups"] = ["main"] locked_package["files"] = [] del lock_data["metadata"]["files"] def test_run_no_dependencies(installer: Installer, locker: Locker) -> None: result = installer.run() assert result == 0 expected = fixture("no-dependencies") assert locker.written_data == expected def test_not_fresh_lock(installer: Installer, locker: Locker) -> None: locker.locked().fresh(False) with pytest.raises( ValueError, match=re.escape( "pyproject.toml changed significantly since poetry.lock was last generated. " "Run `poetry lock` to fix the lock file." ), ): installer.run() def test_not_fresh_lock_self_project(installer: Installer, locker: Locker) -> None: installer.set_package(ProjectPackage(POETRY_SYSTEM_PROJECT_NAME, "1.0")) locker.locked().fresh(False) with pytest.raises( ValueError, match=re.escape( "pyproject.toml changed significantly since poetry.lock was last generated. " "Run `poetry self lock` to fix the lock file." ), ): installer.run() def test_run_with_dependencies( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage ) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") repo.add_package(package_a) repo.add_package(package_b) package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) result = installer.run() assert result == 0 expected = fixture("with-dependencies") assert locker.written_data == expected @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_update_after_removing_dependencies( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "B", "version": "1.1", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "C", "version": "1.2", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": [], "B": [], "C": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") package_c = get_package("C", "1.2") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) installed.add_package(package_a) installed.add_package(package_b) installed.add_package(package_c) package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "~1.1")) installer.update(True) result = installer.run() assert result == 0 expected = fixture("with-dependencies") assert locker.written_data == expected assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 1 def _configure_run_install_dev( lock_version: str, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, with_optional_group: bool = False, with_packages_installed: bool = False, ) -> None: """ Perform common test setup for `test_run_install_*dev*()` methods. """ lock_data: dict[str, Any] = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "B", "version": "1.1", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "C", "version": "1.2", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": [], "B": [], "C": []}, }, } if lock_version == "2.1": for locked_package in lock_data["package"]: locked_package["groups"] = [ "dev" if locked_package["name"] == "C" else "main" ] locked_package["files"] = [] del lock_data["metadata"]["files"] locker.locked(True) locker.mock_lock_data(lock_data) package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") package_c = get_package("C", "1.2") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) if with_packages_installed: installed.add_package(package_a) installed.add_package(package_b) installed.add_package(package_c) package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "~1.1")) group = DependencyGroup("dev", optional=with_optional_group) group.add_dependency(Factory.create_dependency("C", "~1.2", groups=["dev"])) package.add_dependency_group(group) @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) @pytest.mark.parametrize("update", [False, True]) @pytest.mark.parametrize("requires_synchronization", [False, True]) @pytest.mark.parametrize( ("groups", "installs", "updates", "removals", "with_packages_installed"), [ (None, 2, 0, 0, False), (None, 0, 0, 1, True), ([], 0, 0, 0, False), ([], 0, 0, 3, True), (["dev"], 1, 0, 0, False), (["dev"], 0, 0, 2, True), ([MAIN_GROUP], 2, 0, 0, False), ([MAIN_GROUP], 0, 0, 1, True), ([MAIN_GROUP, "dev"], 3, 0, 0, False), ([MAIN_GROUP, "dev"], 0, 0, 0, True), ], ) def test_run_install_with_dependency_groups( groups: list[str] | None, installs: int, updates: int, removals: int, with_packages_installed: bool, installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, update: bool, requires_synchronization: bool, lock_version: str, ) -> None: _configure_run_install_dev( lock_version, locker, repo, package, installed, with_optional_group=True, with_packages_installed=with_packages_installed, ) if groups is not None: installer.only_groups({canonicalize_name(g) for g in groups}) installer.update(update) installer.requires_synchronization(requires_synchronization) result = installer.run() assert result == 0 if not requires_synchronization: removals = 0 assert installer.executor.installations_count == installs assert installer.executor.updates_count == updates assert installer.executor.removals_count == removals @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_install_does_not_remove_locked_packages_if_installed_but_not_required( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, lock_version: str, ) -> None: package_a = get_package("a", "1.0") package_b = get_package("b", "1.1") package_c = get_package("c", "1.2") repo.add_package(package_a) installed.add_package(package_a) repo.add_package(package_b) installed.add_package(package_b) repo.add_package(package_c) installed.add_package(package_c) installed.add_package(package) # Root package never removed. package.add_dependency( Factory.create_dependency(package_a.name, str(package_a.version)) ) lock_data = { "package": [ { "name": package_a.name, "version": package_a.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": package_b.name, "version": package_b.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": package_c.name, "version": package_c.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {package_a.name: [], package_b.name: [], package_c.name: []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) result = installer.run() assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_install_removes_locked_packages_if_installed_and_synchronization_is_required( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, lock_version: str, config_installer_reresolve: bool, ) -> None: package_a = get_package("a", "1.0") package_b = get_package("b", "1.1") package_c = get_package("c", "1.2") repo.add_package(package_a) installed.add_package(package_a) repo.add_package(package_b) installed.add_package(package_b) repo.add_package(package_c) installed.add_package(package_c) installed.add_package(package) # Root package never removed. package.add_dependency( Factory.create_dependency(package_a.name, str(package_a.version)) ) lock_data = { "package": [ { "name": package_a.name, "version": package_a.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": package_b.name, "version": package_b.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": package_c.name, "version": package_c.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {package_a.name: [], package_b.name: [], package_c.name: []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) installer.update(True) installer.requires_synchronization(True) installer.run() assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 2 @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_install_removes_no_longer_locked_packages_if_installed( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, lock_version: str, ) -> None: package_a = get_package("a", "1.0") package_b = get_package("b", "1.1") package_c = get_package("c", "1.2") repo.add_package(package_a) installed.add_package(package_a) repo.add_package(package_b) installed.add_package(package_b) repo.add_package(package_c) installed.add_package(package_c) installed.add_package(package) # Root package never removed. package.add_dependency( Factory.create_dependency(package_a.name, str(package_a.version)) ) lock_data = { "package": [ { "name": package_a.name, "version": package_a.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": package_b.name, "version": package_b.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": package_c.name, "version": package_c.version.text, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {package_a.name: [], package_b.name: [], package_c.name: []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) installer.update(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 2 @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) @pytest.mark.parametrize( "managed_reserved_package_names", [(), ("pip",)], ) def test_run_install_with_synchronization( managed_reserved_package_names: tuple[str, ...], installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, lock_version: str, ) -> None: package_a = get_package("a", "1.0") package_b = get_package("b", "1.1") package_c = get_package("c", "1.2") package_pip = get_package("pip", "20.0.0") all_packages = [ package_a, package_b, package_c, package_pip, ] managed_reserved_packages = [ pkg for pkg in all_packages if pkg.name in managed_reserved_package_names ] locked_packages = [package_a, *managed_reserved_packages] for pkg in all_packages: repo.add_package(pkg) installed.add_package(pkg) installed.add_package(package) # Root package never removed. package.add_dependency( Factory.create_dependency(package_a.name, str(package_a.version)) ) lock_data = { "package": [ { "name": pkg.name, "version": pkg.version, "optional": False, "platform": "*", "python-versions": "*", "checksum": [], } for pkg in locked_packages ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {pkg.name: [] for pkg in locked_packages}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) installer.update(True) installer.requires_synchronization(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 2 + len(managed_reserved_packages) expected_removals = { package_b.name, package_c.name, *managed_reserved_package_names, } assert isinstance(installer.executor, TestExecutor) assert {r.name for r in installer.executor.removals} == expected_removals @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_whitelist_add( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], } ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package_a = get_package("A", "1.0") package_a_new = get_package("A", "1.1") package_b = get_package("B", "1.1") repo.add_package(package_a) repo.add_package(package_a_new) repo.add_package(package_b) package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) installer.update(True) installer.whitelist(["B"]) result = installer.run() assert result == 0 expected = fixture("with-dependencies") assert locker.written_data == expected @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_whitelist_remove( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "B", "version": "1.1", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": [], "B": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") repo.add_package(package_a) repo.add_package(package_b) installed.add_package(package_b) package.add_dependency(Factory.create_dependency("A", "~1.0")) installer.update(True) installer.whitelist(["B"]) result = installer.run() assert result == 0 expected = fixture("remove") assert locker.written_data == expected assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 1 def test_add_with_sub_dependencies( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage ) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") package_c = get_package("C", "1.2") package_d = get_package("D", "1.3") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) package_a.add_dependency(Factory.create_dependency("D", "^1.0")) package_b.add_dependency(Factory.create_dependency("C", "~1.2")) result = installer.run() assert result == 0 expected = fixture("with-sub-dependencies") assert locker.written_data == expected def test_run_with_python_versions( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage ) -> None: package.python_versions = "~2.7 || ^3.4" package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") package_c12 = get_package("C", "1.2") package_c12.python_versions = "~2.7 || ^3.3" package_c13 = get_package("C", "1.3") package_c13.python_versions = "~3.3" repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c12) repo.add_package(package_c13) package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) package.add_dependency(Factory.create_dependency("C", "^1.0")) result = installer.run() assert result == 0 expected = fixture("with-python-versions") assert locker.written_data == expected def test_run_with_optional_and_python_restricted_dependencies( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage ) -> None: package.python_versions = "~2.7 || ^3.4" package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") package_c12 = get_package("C", "1.2") package_c13 = get_package("C", "1.3") package_d = get_package("D", "1.4") package_c13.add_dependency(Factory.create_dependency("D", "^1.2")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c12) repo.add_package(package_c13) repo.add_package(package_d) package.extras = {canonicalize_name("foo"): [get_dependency("A", "~1.0")]} dep_a = Factory.create_dependency("A", {"version": "~1.0", "optional": True}) dep_a._in_extras = [canonicalize_name("foo")] package.add_dependency(dep_a) package.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "~2.4"}) ) package.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "python": "~2.7 || ^3.4"}) ) result = installer.run() assert result == 0 expected = fixture("with-optional-dependencies") assert locker.written_data == expected # We should only have 2 installs: # C,D since python version is not compatible # with B's python constraint and A is optional assert isinstance(installer.executor, TestExecutor) assert installer.executor.installations_count == 2 assert installer.executor.installations[0].name == "d" assert installer.executor.installations[1].name == "c" def test_run_with_optional_and_platform_restricted_dependencies( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, mocker: MockerFixture, ) -> None: mocker.patch("sys.platform", "darwin") package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") package_c12 = get_package("C", "1.2") package_c13 = get_package("C", "1.3") package_d = get_package("D", "1.4") package_c13.add_dependency(Factory.create_dependency("D", "^1.2")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c12) repo.add_package(package_c13) repo.add_package(package_d) package.extras = {canonicalize_name("foo"): [get_dependency("A", "~1.0")]} dep_a = Factory.create_dependency("A", {"version": "~1.0", "optional": True}) dep_a._in_extras = [canonicalize_name("foo")] package.add_dependency(dep_a) package.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "platform": "custom"}) ) package.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "platform": "darwin"}) ) result = installer.run() assert result == 0 expected = fixture("with-platform-dependencies") assert locker.written_data == expected # We should only have 2 installs: # C,D since the mocked python version is not compatible # with B's python constraint and A is optional assert isinstance(installer.executor, TestExecutor) assert installer.executor.installations_count == 2 assert installer.executor.installations[0].name == "d" assert installer.executor.installations[1].name == "c" def test_run_with_dependencies_extras( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage ) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_b.extras = {canonicalize_name("foo"): [get_dependency("C", "^1.0")]} package_b.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "optional": True}) ) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) package.add_dependency(Factory.create_dependency("A", "^1.0")) package.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "extras": ["foo"]}) ) result = installer.run() assert result == 0 expected = fixture("with-dependencies-extras") assert locker.written_data == expected def test_run_with_dependencies_nested_extras( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage ) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") dependency_c = Factory.create_dependency("C", {"version": "^1.0", "optional": True}) dependency_b = Factory.create_dependency( "B", {"version": "^1.0", "optional": True, "extras": ["C"]} ) dependency_a = Factory.create_dependency("A", {"version": "^1.0", "extras": ["B"]}) package_b.extras = {canonicalize_name("c"): [dependency_c]} package_b.add_dependency(dependency_c) package_a.add_dependency(dependency_b) package_a.extras = {canonicalize_name("b"): [dependency_b]} repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) package.add_dependency(dependency_a) result = installer.run() assert result == 0 expected = fixture("with-dependencies-nested-extras") assert locker.written_data == expected @pytest.mark.parametrize( "enabled_extras", [ ([]), (["all"]), (["nested"]), (["install", "download"]), (["install"]), (["download"]), ], ) @pytest.mark.parametrize("top_level_dependency", [True, False]) def test_solver_resolves_self_referential_extras( enabled_extras: list[str], top_level_dependency: bool, installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, create_package: PackageFactory, ) -> None: dependency = ( create_package( "A", str(package.version), extras={ "download": ["download-package"], "install": ["install-package"], "py38": ["py38-package ; python_version == '3.8'"], "py310": ["py310-package ; python_version > '3.8'"], "all": ["a[download,install]"], "py": ["a[py38,py310]"], "nested": ["a[all]"], }, ) .to_dependency() .with_features(enabled_extras) ) if not top_level_dependency: dependency = create_package( "B", "1.0", dependencies=[dependency] ).to_dependency() package.add_dependency(dependency) result = installer.run() assert result == 0 name = "-".join( [ "with-self-referencing-extras", *enabled_extras, "top" if top_level_dependency else "deep", ] ) expected = fixture(name) assert locker.written_data == expected def test_solver_resolves_self_referential_extras_with_markers( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, create_package: PackageFactory, ) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "*", "extras": ["all"]}) ) create_package( "A", str(package.version), extras={ "download": ["download-package"], "install": ["install-package"], "all": ["a[download,install] ; python_version < '3.9'"], }, ) result = installer.run() assert result == 0 name = "-".join(["with-self-referencing-extras", "b", "markers"]) # FIXME: At the time of writing this test case, the markers from self-ref extras are not # correctly propagated into the dependency specs. For example, given this case, # the package "install-package" should have a final marker of # "extra == 'install' or extra == 'all' and python_version < '3.9'". expected = fixture(name) assert locker.written_data == expected @pytest.mark.parametrize("root", [True, False]) @pytest.mark.parametrize("locked", [False, True]) @pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"]) def test_run_with_conflicting_dependency_extras( installer: Installer, pool: RepositoryPool, locker: Locker, installed: CustomInstalledRepository, repo: Repository, config: Config, package: ProjectPackage, extra: str | None, locked: bool, root: bool, ) -> None: """ - https://github.com/python-poetry/poetry/issues/6419 Tests resolution of extras with conflicting dependencies. Tests in both as direct dependencies of root package and as transitive dependencies. """ # A package with two optional dependencies, one for each extra # If root, this is the root package, otherwise an intermediate package main_package = package if root else get_package("intermediate-dep", "1.0.0") # Two conflicting versions of a dependency, one in each extra conflicting_dep_one_pkg = get_package("conflicting-dep", "1.1.0") conflicting_dep_two_pkg = get_package("conflicting-dep", "1.2.0") conflicting_dep_one = Factory.create_dependency( "conflicting-dep", { "version": "1.1.0", "markers": "extra == 'extra-one' and extra != 'extra-two'", "optional": True, }, ) conflicting_dep_two = Factory.create_dependency( "conflicting-dep", { "version": "1.2.0", "markers": "extra != 'extra-one' and extra == 'extra-two'", "optional": True, }, ) # Include both just for extra validation that our marker validation works as expected main_package.extras = { canonicalize_name("extra-one"): [conflicting_dep_one, conflicting_dep_two], canonicalize_name("extra-two"): [conflicting_dep_one, conflicting_dep_two], } main_package.add_dependency(conflicting_dep_one) main_package.add_dependency(conflicting_dep_two) repo.add_package(conflicting_dep_one_pkg) repo.add_package(conflicting_dep_two_pkg) if not root: repo.add_package(main_package) # If we have an intermediate package, add extras to our root package if not root: extra_one_dep = Factory.create_dependency( "intermediate-dep", { "version": "1.0.0", "markers": "extra == 'root-extra-one' and extra != 'root-extra-two'", "extras": ["extra-one"], "optional": True, }, ) extra_two_dep = Factory.create_dependency( "intermediate-dep", { "version": "1.0.0", "markers": "extra != 'root-extra-one' and extra == 'root-extra-two'", "extras": ["extra-two"], "optional": True, }, ) package.add_dependency(extra_one_dep) package.add_dependency(extra_two_dep) # Include both just for extra validation that our marker validation works as expected package.extras = { canonicalize_name("root-extra-one"): [extra_one_dep, extra_two_dep], canonicalize_name("root-extra-two"): [extra_one_dep, extra_two_dep], } fixture_name = "with-conflicting-dependency-extras-" + ( "root" if root else "transitive" ) locker.locked(locked) if locked: locker.mock_lock_data(dict(fixture(fixture_name))) if extra is not None: extras = [f"root-{extra}"] if not root else [extra] installer.extras(extras) result = installer.run() assert result == 0 if not locked: expected = fixture(fixture_name) assert locker.written_data == expected # Results of installation are consistent with the 'extra' input assert isinstance(installer.executor, TestExecutor) expected_installations = [] if extra == "extra-one": expected_installations.append(conflicting_dep_one_pkg) elif extra == "extra-two": expected_installations.append(conflicting_dep_two_pkg) if not root and extra is not None: expected_installations.append(get_package("intermediate-dep", "1.0.0")) assert len(installer.executor.installations) == len(expected_installations) assert set(installer.executor.installations) == set(expected_installations) @pytest.mark.parametrize("locked", [True, False]) @pytest.mark.parametrize("extra", [None, "cpu", "cuda"]) def test_run_with_exclusive_extras_different_sources( installer: Installer, locker: Locker, installed: CustomInstalledRepository, config: Config, package: ProjectPackage, extra: str | None, locked: bool, ) -> None: """ - https://github.com/python-poetry/poetry/issues/6409 - https://github.com/python-poetry/poetry/issues/6419 - https://github.com/python-poetry/poetry/issues/7748 - https://github.com/python-poetry/poetry/issues/9537 """ # Setup repo for each of our sources cpu_repo = Repository("pytorch-cpu") cuda_repo = Repository("pytorch-cuda") pool = RepositoryPool() pool.add_repository(cpu_repo) pool.add_repository(cuda_repo) config.config["repositories"] = { "pytorch-cpu": {"url": "https://download.pytorch.org/whl/cpu"}, "pytorch-cuda": {"url": "https://download.pytorch.org/whl/cuda"}, } # Configure packages that read from each of the different sources torch_cpu_pkg = get_package("torch", "1.11.0+cpu") torch_cpu_pkg._source_reference = "pytorch-cpu" torch_cpu_pkg._source_type = "legacy" torch_cpu_pkg._source_url = "https://download.pytorch.org/whl/cpu" torch_cuda_pkg = get_package("torch", "1.11.0+cuda") torch_cuda_pkg._source_reference = "pytorch-cuda" torch_cuda_pkg._source_type = "legacy" torch_cuda_pkg._source_url = "https://download.pytorch.org/whl/cuda" cpu_repo.add_package(torch_cpu_pkg) cuda_repo.add_package(torch_cuda_pkg) # Depend on each package based on exclusive extras torch_cpu_dep = Factory.create_dependency( "torch", { "version": "1.11.0+cpu", "markers": "extra == 'cpu' and extra != 'cuda'", "source": "pytorch-cpu", }, ) torch_cuda_dep = Factory.create_dependency( "torch", { "version": "1.11.0+cuda", "markers": "extra != 'cpu' and extra == 'cuda'", "source": "pytorch-cuda", }, ) package.add_dependency(torch_cpu_dep) package.add_dependency(torch_cuda_dep) # We don't want to cheat by only including the correct dependency in the 'extra' mapping package.extras = { canonicalize_name("cpu"): [torch_cpu_dep, torch_cuda_dep], canonicalize_name("cuda"): [torch_cpu_dep, torch_cuda_dep], } # Set locker state locker.locked(locked) if locked: locker.mock_lock_data(dict(fixture("with-exclusive-extras"))) # Perform install installer = Installer( NullIO(), MockEnv(), package, locker, pool, config, installed=installed, executor=TestExecutor( MockEnv(), pool, config, NullIO(), ), ) if extra is not None: installer.extras([extra]) result = installer.run() assert result == 0 # Results of locking are expected and installation are consistent with the 'extra' input if not locked: expected = fixture("with-exclusive-extras") assert locker.written_data == expected assert isinstance(installer.executor, TestExecutor) if extra is None: assert len(installer.executor.installations) == 0 else: assert len(installer.executor.installations) == 1 version = f"1.11.0+{extra}" source_url = f"https://download.pytorch.org/whl/{extra}" source_reference = f"pytorch-{extra}" assert installer.executor.installations[0] == Package( "torch", version, source_type="legacy", source_url=source_url, source_reference=source_reference, ) @pytest.mark.parametrize("locked", [True, False]) @pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"]) def test_run_with_different_dependency_extras( installer: Installer, pool: RepositoryPool, locker: Locker, installed: CustomInstalledRepository, repo: Repository, config: Config, package: ProjectPackage, extra: str | None, locked: bool, ) -> None: """ - https://github.com/python-poetry/poetry/issues/834 - https://github.com/python-poetry/poetry/issues/7748 This tests different sets of extras in a dependency of the root project. These different dependency extras are themselves conditioned on extras in the root project. """ # Three packages in addition to root: demo (direct dependency) and two transitive dep packages demo_pkg = get_package("demo", "1.0.0") transitive_one_pkg = get_package("transitive-dep-one", "1.1.0") transitive_two_pkg = get_package("transitive-dep-two", "1.2.0") # Switch each transitive dependency based on extra markers in the 'demo' package transitive_dep_one = Factory.create_dependency( "transitive-dep-one", { "version": "1.1.0", "markers": "extra == 'demo-extra-one' and extra != 'demo-extra-two'", "optional": True, }, ) transitive_dep_two = Factory.create_dependency( "transitive-dep-two", { "version": "1.2.0", "markers": "extra != 'demo-extra-one' and extra == 'demo-extra-two'", "optional": True, }, ) # Include both packages in both demo extras, to validate that they're filtered out based on extra markers alone demo_pkg.extras = { canonicalize_name("demo-extra-one"): [ get_dependency("transitive-dep-one"), get_dependency("transitive-dep-two"), ], canonicalize_name("demo-extra-two"): [ get_dependency("transitive-dep-one"), get_dependency("transitive-dep-two"), ], } demo_pkg.add_dependency(transitive_dep_one) demo_pkg.add_dependency(transitive_dep_two) # Now define the demo dependency, similarly switched on extra markers in the root package extra_one_dep = Factory.create_dependency( "demo", { "version": "1.0.0", "markers": "extra == 'extra-one' and extra != 'extra-two'", "extras": ["demo-extra-one"], }, ) extra_two_dep = Factory.create_dependency( "demo", { "version": "1.0.0", "markers": "extra != 'extra-one' and extra == 'extra-two'", "extras": ["demo-extra-two"], }, ) package.add_dependency(extra_one_dep) package.add_dependency(extra_two_dep) # Again we don't want to cheat by only including the correct dependency in the 'extra' mapping package.extras = { canonicalize_name("extra-one"): [extra_one_dep, extra_two_dep], canonicalize_name("extra-two"): [extra_one_dep, extra_two_dep], } repo.add_package(demo_pkg) repo.add_package(transitive_one_pkg) repo.add_package(transitive_two_pkg) locker.locked(locked) if locked: locker.mock_lock_data(dict(fixture("with-dependencies-differing-extras"))) installer = Installer( NullIO(), MockEnv(), package, locker, pool, config, installed=installed, executor=TestExecutor( MockEnv(), pool, config, NullIO(), ), ) if extra is not None: installer.extras([extra]) result = installer.run() assert result == 0 if not locked: expected = fixture("with-dependencies-differing-extras") assert locker.written_data == expected # Results of installation are consistent with the 'extra' input assert isinstance(installer.executor, TestExecutor) if extra is None: assert len(installer.executor.installations) == 0 else: assert len(installer.executor.installations) == 2 @pytest.mark.parametrize("is_locked", [False, True]) @pytest.mark.parametrize("is_installed", [False, True]) @pytest.mark.parametrize("with_extras", [False, True]) @pytest.mark.parametrize("do_update", [False, True]) @pytest.mark.parametrize("do_sync", [False, True]) def test_run_installs_extras_with_deps_if_requested( installer: Installer, locker: Locker, repo: Repository, installed: CustomInstalledRepository, package: ProjectPackage, is_locked: bool, is_installed: bool, with_extras: bool, do_update: bool, do_sync: bool, ) -> None: package.extras = {canonicalize_name("foo"): [get_dependency("C")]} package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_d = get_package("D", "1.1") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) package.add_dependency(Factory.create_dependency("A", "^1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) dep_c = Factory.create_dependency("C", {"version": "^1.0", "optional": True}) dep_c._in_extras = [canonicalize_name("foo")] package.add_dependency(dep_c) package_c.add_dependency(Factory.create_dependency("D", "^1.0")) if is_locked: locker.locked(True) locker.mock_lock_data(fixture("extras-with-dependencies")) if is_installed: installed.add_package(package_a) installed.add_package(package_b) installed.add_package(package_c) installed.add_package(package_d) if with_extras: installer.extras(["foo"]) installer.update(do_update) installer.requires_synchronization(do_sync) result = installer.run() assert result == 0 if not is_locked: assert locker.written_data == fixture("extras-with-dependencies") if with_extras: # A, B, C, D expected_installations_count = 0 if is_installed else 4 expected_removals_count = 0 else: # A, B expected_installations_count = 0 if is_installed else 2 # We only want to uninstall extras if we do a "poetry install" without extras, # not if we do a "poetry update" or "poetry add". expected_removals_count = 2 if is_installed and do_sync else 0 assert installer.executor.installations_count == expected_installations_count assert installer.executor.removals_count == expected_removals_count def test_installer_with_pypi_repository( package: ProjectPackage, locker: Locker, installed: CustomInstalledRepository, config: Config, env: NullEnv, pypi_repository: PyPiRepository, ) -> None: pool = RepositoryPool() pool.add_repository(pypi_repository) installer = Installer( NullIO(), env, package, locker, pool, config, installed=installed ) package.python_versions = ">=3.7" package.add_dependency(Factory.create_dependency("pytest", "^3.5", groups=["dev"])) result = installer.run() assert result == 0 expected = fixture("with-pypi-repository") assert locker.written_data == expected def test_run_installs_with_local_file( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, ) -> None: root_dir = Path(__file__).parent.parent.parent package.root_dir = root_dir locker.set_lock_path(root_dir) file_path = fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") package.add_dependency( Factory.create_dependency( "demo", {"file": str(file_path.relative_to(root_dir))}, root_dir=root_dir ) ) repo.add_package(get_package("pendulum", "1.4.4")) result = installer.run() assert result == 0 expected = fixture("with-file-dependency") assert locker.written_data == expected assert installer.executor.installations_count == 2 def test_run_installs_wheel_with_no_requires_dist( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, ) -> None: root_dir = Path(__file__).parent.parent.parent package.root_dir = root_dir locker.set_lock_path(root_dir) file_path = fixture_dir( "wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.whl" ) package.add_dependency( Factory.create_dependency( "demo", {"file": str(file_path.relative_to(root_dir))}, root_dir=root_dir ) ) result = installer.run() assert result == 0 expected = fixture("with-wheel-dependency-no-requires-dist") assert locker.written_data == expected assert installer.executor.installations_count == 1 def test_run_installs_with_local_poetry_directory_and_extras( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, tmpdir: Path, fixture_dir: FixtureDirGetter, ) -> None: root_dir = Path(__file__).parent.parent.parent package.root_dir = root_dir locker.set_lock_path(root_dir) file_path = fixture_dir("project_with_extras") package.add_dependency( Factory.create_dependency( "project-with-extras", {"path": str(file_path.relative_to(root_dir)), "extras": ["extras_a"]}, root_dir=root_dir, ) ) repo.add_package(get_package("pendulum", "1.4.4")) result = installer.run() assert result == 0 expected = fixture("with-directory-dependency-poetry") assert locker.written_data == expected assert installer.executor.installations_count == 2 @pytest.mark.parametrize("skip_directory", [True, False]) def test_run_installs_with_local_poetry_directory_and_skip_directory_flag( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, skip_directory: bool, ) -> None: """When we set Installer.skip_directory(True) no path dependencies should be installed (including transitive dependencies). """ root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) directory = root_dir.joinpath("project_with_transitive_directory_dependencies") package.add_dependency( Factory.create_dependency( "project-with-transitive-directory-dependencies", {"path": str(directory.relative_to(root_dir))}, root_dir=root_dir, ) ) repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) installer.skip_directory(skip_directory) result = installer.run() assert result == 0 expected = fixture("with-directory-dependency-poetry-transitive") assert locker.written_data == expected assert isinstance(installer.executor, TestExecutor) directory_installs = [ p.name for p in installer.executor.installations if p.source_type == "directory" ] if skip_directory: assert not directory_installs, directory_installs assert installer.executor.installations_count == 2 else: assert directory_installs, directory_installs assert installer.executor.installations_count == 6 def test_run_installs_with_local_poetry_file_transitive( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, tmpdir: str, fixture_dir: FixtureDirGetter, ) -> None: root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) directory = fixture_dir("directory").joinpath( "project_with_transitive_file_dependencies" ) package.add_dependency( Factory.create_dependency( "project-with-transitive-file-dependencies", {"path": str(directory.relative_to(root_dir))}, root_dir=root_dir, ) ) repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) result = installer.run() assert result == 0 expected = fixture("with-file-dependency-transitive") assert locker.written_data == expected assert installer.executor.installations_count == 4 def test_run_installs_with_local_setuptools_directory( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, tmp_path: Path, fixture_dir: FixtureDirGetter, ) -> None: root_dir = tmp_path / "root" package.root_dir = root_dir locker.set_lock_path(root_dir) file_path = shutil.copytree(fixture_dir("project_with_setup"), root_dir / "project") package.add_dependency( Factory.create_dependency( "project-with-setup", {"path": str(file_path.relative_to(root_dir))}, root_dir=root_dir, ) ) repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) result = installer.run() assert result == 0 expected = fixture("with-directory-dependency-setuptools") assert locker.written_data == expected assert installer.executor.installations_count == 3 @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_with_prereleases( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0a2", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], } ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package_a = get_package("A", "1.0a2") package_b = get_package("B", "1.1") repo.add_package(package_a) repo.add_package(package_b) package.add_dependency( Factory.create_dependency("A", {"version": "*", "allow-prereleases": True}) ) package.add_dependency(Factory.create_dependency("B", "^1.1")) installer.update(True) installer.whitelist({"B": "^1.1"}) result = installer.run() assert result == 0 expected = fixture("with-prereleases") assert locker.written_data == expected @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_update_all_with_lock( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": True, "platform": "*", "python-versions": "*", "checksum": [], } ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package_a = get_package("A", "1.1") repo.add_package(get_package("A", "1.0")) repo.add_package(package_a) package.add_dependency(Factory.create_dependency("A", "*")) installer.update(True) result = installer.run() assert result == 0 expected = fixture("update-with-lock") assert locker.written_data == expected @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_update_with_locked_extras( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"B": "^1.0", "C": "^1.0"}, }, { "name": "B", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "C", "version": "1.1", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "requirements": {"python": "~2.7"}, }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": [], "B": [], "C": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package_a = get_package("A", "1.0") package_a.extras = {canonicalize_name("foo"): [get_dependency("B")]} b_dependency = get_dependency("B", "^1.0", optional=True) b_dependency._in_extras = [canonicalize_name("foo")] c_dependency = get_dependency("C", "^1.0") c_dependency.python_versions = "~2.7" package_a.add_dependency(b_dependency) package_a.add_dependency(c_dependency) repo.add_package(package_a) repo.add_package(get_package("B", "1.0")) repo.add_package(get_package("C", "1.1")) repo.add_package(get_package("D", "1.1")) package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "extras": ["foo"]}) ) package.add_dependency(Factory.create_dependency("D", "^1.0")) installer.update(True) installer.whitelist("D") result = installer.run() assert result == 0 expected = fixture("update-with-locked-extras") assert locker.written_data == expected def test_run_install_duplicate_dependencies_different_constraints( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<4.0"}) ) package_a.add_dependency( Factory.create_dependency("B", {"version": "^2.0", "python": ">=4.0"}) ) package_b10 = get_package("B", "1.0") package_b20 = get_package("B", "2.0") package_b10.add_dependency(Factory.create_dependency("C", "1.2")) package_b20.add_dependency(Factory.create_dependency("C", "1.5")) package_c12 = get_package("C", "1.2") package_c15 = get_package("C", "1.5") repo.add_package(package_a) repo.add_package(package_b10) repo.add_package(package_b20) repo.add_package(package_c12) repo.add_package(package_c15) result = installer.run() assert result == 0 expected = fixture("with-duplicate-dependencies") assert locker.written_data == expected assert isinstance(installer.executor, TestExecutor) installs = installer.executor.installations assert installer.executor.installations_count == 3 assert installs[0] == package_c12 assert installs[1] == package_b10 assert installs[2] == package_a assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_install_duplicate_dependencies_different_constraints_with_lock( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": { "B": [ {"version": "^1.0", "python": "<4.0"}, {"version": "^2.0", "python": ">=4.0"}, ] }, }, { "name": "B", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"C": "1.2"}, "requirements": {"python": "<4.0"}, }, { "name": "B", "version": "2.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"C": "1.5"}, "requirements": {"python": ">=4.0"}, }, { "name": "C", "version": "1.2", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "C", "version": "1.5", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": [], "B": [], "C": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<4.0"}) ) package_a.add_dependency( Factory.create_dependency("B", {"version": "^2.0", "python": ">=4.0"}) ) package_b10 = get_package("B", "1.0") package_b20 = get_package("B", "2.0") package_b10.add_dependency(Factory.create_dependency("C", "1.2")) package_b20.add_dependency(Factory.create_dependency("C", "1.5")) package_c12 = get_package("C", "1.2") package_c15 = get_package("C", "1.5") repo.add_package(package_a) repo.add_package(package_b10) repo.add_package(package_b20) repo.add_package(package_c12) repo.add_package(package_c15) installer.update(True) result = installer.run() assert result == 0 expected = fixture("with-duplicate-dependencies") assert locker.written_data == expected assert installer.executor.installations_count == 3 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_update_uninstalls_after_removal_transitive_dependency( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"B": {"version": "^1.0", "python": "<2.0"}}, }, { "name": "B", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": [], "B": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<2.0"}) ) package_b10 = get_package("B", "1.0") repo.add_package(package_a) repo.add_package(package_b10) installed.add_package(get_package("A", "1.0")) installed.add_package(get_package("B", "1.0")) installer.update(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 1 @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_run_install_duplicate_dependencies_different_constraints_with_lock_update( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": { "B": [ {"version": "^1.0", "python": "<2.7"}, {"version": "^2.0", "python": ">=2.7"}, ] }, }, { "name": "B", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"C": "1.2"}, "requirements": {"python": "<2.7"}, }, { "name": "B", "version": "2.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"C": "1.5"}, "requirements": {"python": ">=2.7"}, }, { "name": "C", "version": "1.2", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, { "name": "C", "version": "1.5", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": [], "B": [], "C": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.1") package_a.add_dependency(Factory.create_dependency("B", "^2.0")) package_b10 = get_package("B", "1.0") package_b20 = get_package("B", "2.0") package_b10.add_dependency(Factory.create_dependency("C", "1.2")) package_b20.add_dependency(Factory.create_dependency("C", "1.5")) package_c12 = get_package("C", "1.2") package_c15 = get_package("C", "1.5") repo.add_package(package_a) repo.add_package(package_b10) repo.add_package(package_b20) repo.add_package(package_c12) repo.add_package(package_c15) installed.add_package(get_package("A", "1.0")) installer.update(True) installer.whitelist(["A"]) result = installer.run() assert result == 0 expected = fixture("with-duplicate-dependencies-update") assert locker.written_data == expected assert installer.executor.installations_count == 2 assert installer.executor.updates_count == 1 assert installer.executor.removals_count == 0 def test_installer_test_solver_finds_compatible_package_for_dependency_python_not_fully_compatible_with_package_python( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, ) -> None: package.python_versions = "~2.7 || ^3.4" package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.5"}) ) package_a101 = get_package("A", "1.0.1") package_a101.python_versions = ">=3.6" package_a100 = get_package("A", "1.0.0") package_a100.python_versions = ">=3.5" repo.add_package(package_a100) repo.add_package(package_a101) result = installer.run() assert result == 0 expected = fixture("with-conditional-dependency") assert locker.written_data == expected assert installer.executor.installations_count == 1 def test_installer_required_extras_should_not_be_removed_when_updating_single_dependency( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, env: NullEnv, pool: RepositoryPool, config: Config, ) -> None: package.add_dependency(Factory.create_dependency("A", {"version": "^1.0"})) package_a = get_package("A", "1.0.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "extras": ["foo"]}) ) package_b = get_package("B", "1.0.0") package_b.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "optional": True}) ) package_b.extras = {canonicalize_name("foo"): [get_dependency("C")]} package_c = get_package("C", "1.0.0") package_d = get_package("D", "1.0.0") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) installer.update(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 3 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 package.add_dependency(Factory.create_dependency("D", "^1.0")) locker.locked(True) locker.mock_lock_data(locker.written_data) installed.add_package(package_a) installed.add_package(package_b) installed.add_package(package_c) installer = Installer( NullIO(), env, package, locker, pool, config, installed=installed, executor=TestExecutor(env, pool, config, NullIO()), ) installer.update(True) installer.whitelist(["D"]) result = installer.run() assert result == 0 assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 def test_installer_required_extras_should_not_be_removed_when_updating_single_dependency_pypi_repository( locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, env: NullEnv, mocker: MockerFixture, config: Config, pypi_repository: PyPiRepository, ) -> None: mocker.patch("sys.platform", "darwin") pool = RepositoryPool() pool.add_repository(pypi_repository) installer = Installer( NullIO(), env, package, locker, pool, config, installed=installed, executor=TestExecutor(env, pool, config, NullIO()), ) package.add_dependency( Factory.create_dependency( "with-transitive-extra-dependency", {"version": "^0.12"} ) ) installer.update(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 3 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 package.add_dependency(Factory.create_dependency("pytest", "^3.5")) locker.locked(True) locker.mock_lock_data(locker.written_data) assert isinstance(installer.executor, TestExecutor) for pkg in installer.executor.installations: installed.add_package(pkg) installer = Installer( NullIO(), env, package, locker, pool, config, installed=installed, executor=TestExecutor(env, pool, config, NullIO()), ) installer.update(True) installer.whitelist(["pytest"]) result = installer.run() assert result == 0 assert installer.executor.installations_count == 7 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 def test_installer_required_extras_should_be_installed( locker: Locker, repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, env: NullEnv, config: Config, pypi_repository: PyPiRepository, ) -> None: pool = RepositoryPool() pool.add_repository(pypi_repository) installer = Installer( NullIO(), env, package, locker, pool, config, installed=installed, executor=TestExecutor(env, pool, config, NullIO()), ) package.add_dependency( Factory.create_dependency( "with-extra-dependency", {"version": "^0.12", "extras": ["filecache"]} ) ) installer.update(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 2 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 locker.locked(True) locker.mock_lock_data(locker.written_data) installer = Installer( NullIO(), env, package, locker, pool, config, installed=installed, executor=TestExecutor(env, pool, config, NullIO()), ) installer.update(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 2 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_update_multiple_times_with_split_dependencies_is_idempotent( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "A", "version": "1.0", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"B": ">=1.0"}, }, { "name": "B", "version": "1.0.1", "optional": False, "platform": "*", "python-versions": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", "checksum": [], "dependencies": {}, }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", "files": {"A": [], "B": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package.python_versions = "~2.7 || ^3.4" package.add_dependency(Factory.create_dependency("A", "^1.0")) a10 = get_package("A", "1.0") a11 = get_package("A", "1.1") a11.add_dependency(Factory.create_dependency("B", ">=1.0.1")) a11.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "python": "~2.7"}) ) a11.add_dependency( Factory.create_dependency("C", {"version": "^2.0", "python": "^3.4"}) ) b101 = get_package("B", "1.0.1") b110 = get_package("B", "1.1.0") repo.add_package(a10) repo.add_package(a11) repo.add_package(b101) repo.add_package(b110) repo.add_package(get_package("C", "1.0")) repo.add_package(get_package("C", "2.0")) expected = fixture("with-multiple-updates") installer.update(True) result = installer.run() assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) result = installer.run() assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) result = installer.run() assert result == 0 assert locker.written_data == expected def test_installer_can_install_dependencies_from_forced_source( locker: Locker, package: ProjectPackage, installed: CustomInstalledRepository, env: NullEnv, config: Config, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, ) -> None: package.python_versions = "^3.7" package.add_dependency( Factory.create_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) ) pool = RepositoryPool() pool.add_repository(legacy_repository) pool.add_repository(pypi_repository) installer = Installer( NullIO(), env, package, locker, pool, config, installed=installed, executor=TestExecutor(env, pool, config, NullIO()), ) installer.update(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 def test_run_installs_with_url_file( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage ) -> None: url = "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" package.add_dependency(Factory.create_dependency("demo", {"url": url})) repo.add_package(get_package("pendulum", "1.4.4")) result = installer.run() assert result == 0 expected = fixture("with-url-dependency") assert locker.written_data == expected assert installer.executor.installations_count == 2 @pytest.mark.parametrize("env_platform", ["linux", "win32"]) def test_run_installs_with_same_version_url_files( pool: RepositoryPool, locker: Locker, installed: CustomInstalledRepository, config: Config, repo: Repository, package: ProjectPackage, env_platform: str, ) -> None: urls = { "linux": "https://files.pythonhosted.org/distributions/demo-0.1.0.tar.gz", "win32": ( "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" ), } for platform, url in urls.items(): package.add_dependency( Factory.create_dependency( "demo", {"url": url, "markers": f"sys_platform == '{platform}'"}, ) ) repo.add_package(get_package("pendulum", "1.4.4")) installer = Installer( NullIO(), MockEnv(platform=env_platform), package, locker, pool, config, installed=installed, executor=TestExecutor( MockEnv(platform=env_platform), pool, config, NullIO(), ), ) result = installer.run() assert result == 0 expected = fixture("with-same-version-url-dependencies") assert locker.written_data == expected assert isinstance(installer.executor, TestExecutor) assert installer.executor.installations_count == 2 demo_package = next(p for p in installer.executor.installations if p.name == "demo") assert demo_package.source_url == urls[env_platform] def test_installer_uses_prereleases_if_they_are_compatible( installer: Installer, locker: Locker, package: ProjectPackage, repo: Repository ) -> None: package.python_versions = "~2.7 || ^3.4" package.add_dependency( Factory.create_dependency( "prerelease", {"git": "https://github.com/demo/prerelease.git"} ) ) package_b = get_package("b", "2.0.0") package_b.add_dependency(Factory.create_dependency("prerelease", ">=0.19")) repo.add_package(package_b) result = installer.run() assert result == 0 locker.locked(True) locker.mock_lock_data(locker.written_data) package.add_dependency(Factory.create_dependency("b", "^2.0.0")) installer.whitelist(["b"]) installer.update(True) result = installer.run() assert result == 0 assert installer.executor.installations_count == 2 def test_installer_does_not_write_lock_file_when_installation_fails( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, mocker: MockerFixture, ) -> None: repo.add_package(get_package("A", "1.0")) package.add_dependency(Factory.create_dependency("A", "~1.0")) locker.locked(False) mocker.patch("poetry.installation.installer.Installer._execute", return_value=1) result = installer.run() assert result == 1 # error assert locker._lock_data is None assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 assert installer.executor.removals_count == 0 @pytest.mark.parametrize("quiet", [True, False]) def test_run_with_dependencies_quiet( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, quiet: bool, ) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") repo.add_package(package_a) repo.add_package(package_b) installer._io = BufferedIO(Input()) installer._io.set_verbosity(Verbosity.QUIET if quiet else Verbosity.NORMAL) package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) result = installer.run() assert result == 0 expected = fixture("with-dependencies") assert locker.written_data == expected output = installer._io.fetch_output() if quiet: assert output == "" else: assert output != "" @pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_installer_should_use_the_locked_version_of_git_dependencies( installer: Installer, locker: Locker, package: ProjectPackage, repo: Repository, lock_version: str, ) -> None: lock_data = { "package": [ { "name": "demo", "version": "0.1.1", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {"pendulum": ">=1.4.4"}, "source": { "type": "git", "url": "https://github.com/demo/demo.git", "reference": "master", "resolved_reference": "123456", }, }, { "name": "pendulum", "version": "1.4.4", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], "dependencies": {}, }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "platform": "*", "content-hash": "123456789", "files": {"demo": [], "pendulum": []}, }, } fix_lock_data(lock_data) locker.locked(True) locker.mock_lock_data(lock_data) package.add_dependency( Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git", "branch": "master"} ) ) repo.add_package(get_package("pendulum", "1.4.4")) result = installer.run() assert result == 0 assert isinstance(installer.executor, TestExecutor) demo_installation = next( package for package in installer.executor.installations if package.name == "demo" ) assert demo_installation == Package( "demo", "0.1.1", source_type="git", source_url="https://github.com/demo/demo.git", source_reference="master", source_resolved_reference="123456", ) @pytest.mark.parametrize("is_locked", [False, True]) def test_installer_should_use_the_locked_version_of_git_dependencies_with_extras( installer: Installer, locker: Locker, package: ProjectPackage, repo: Repository, is_locked: bool, ) -> None: if is_locked: locker.locked(True) locker.mock_lock_data(fixture("with-vcs-dependency-with-extras")) expected_reference = "123456" else: expected_reference = MOCK_DEFAULT_GIT_REVISION package.add_dependency( Factory.create_dependency( "demo", { "git": "https://github.com/demo/demo.git", "branch": "master", "extras": ["foo"], }, ) ) repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "1.0.0")) result = installer.run() assert result == 0 assert isinstance(installer.executor, TestExecutor) assert len(installer.executor.installations) == 3 demo_installation = next( package for package in installer.executor.installations if package.name == "demo" ) assert demo_installation == Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference="master", source_resolved_reference=expected_reference, ) @pytest.mark.parametrize("is_locked", [False, True]) def test_installer_should_use_the_locked_version_of_git_dependencies_without_reference( installer: Installer, locker: Locker, package: ProjectPackage, repo: Repository, is_locked: bool, ) -> None: """ If there is no explicit reference (branch or tag or rev) in pyproject.toml, HEAD is used. """ if is_locked: locker.locked(True) locker.mock_lock_data(fixture("with-vcs-dependency-without-ref")) expected_reference = "123456" else: expected_reference = MOCK_DEFAULT_GIT_REVISION package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) repo.add_package(get_package("pendulum", "1.4.4")) result = installer.run() assert result == 0 assert isinstance(installer.executor, TestExecutor) assert len(installer.executor.installations) == 2 demo_installation = next( package for package in installer.executor.installations if package.name == "demo" ) assert demo_installation == Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference="HEAD", source_resolved_reference=expected_reference, ) @pytest.mark.parametrize("lock_version", ("2.0", "2.1")) @pytest.mark.parametrize("env_platform", ["darwin", "linux"]) def test_installer_distinguishes_locked_packages_with_local_version_by_source( pool: RepositoryPool, locker: Locker, installed: CustomInstalledRepository, config: Config, repo: Repository, package: ProjectPackage, env_platform: str, lock_version: str, ) -> None: """https://github.com/python-poetry/poetry/issues/6710""" # Require 1.11.0+cpu from pytorch for most platforms, but specify 1.11.0 and pypi on # darwin. package.add_dependency( Factory.create_dependency( "torch", { "version": "1.11.0+cpu", "markers": "sys_platform != 'darwin'", "source": "pytorch", }, ) ) package.add_dependency( Factory.create_dependency( "torch", { "version": "1.11.0", "markers": "sys_platform == 'darwin'", "source": "pypi", }, ) ) # Locking finds both the pypi and the pytorch packages. lock_data: dict[str, Any] = { "package": [ { "name": "torch", "version": "1.11.0", "optional": False, "files": [], "python-versions": "*", }, { "name": "torch", "version": "1.11.0+cpu", "optional": False, "files": [], "python-versions": "*", "source": { "type": "legacy", "url": "https://download.pytorch.org/whl", "reference": "pytorch", }, }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", }, } if lock_version == "2.1": lock_data["package"][0]["groups"] = ["main"] lock_data["package"][0]["markers"] = "sys_platform == 'darwin'" lock_data["package"][1]["groups"] = ["main"] lock_data["package"][1]["markers"] = "sys_platform != 'darwin'" locker.locked(True) locker.mock_lock_data(lock_data) installer = Installer( NullIO(), MockEnv(platform=env_platform), package, locker, pool, config, installed=installed, executor=TestExecutor( MockEnv(platform=env_platform), pool, config, NullIO(), ), ) result = installer.run() assert result == 0 # Results of installation are consistent with the platform requirements. version = "1.11.0" if env_platform == "darwin" else "1.11.0+cpu" source_type = None if env_platform == "darwin" else "legacy" source_url = ( None if env_platform == "darwin" else "https://download.pytorch.org/whl" ) source_reference = None if env_platform == "darwin" else "pytorch" assert isinstance(installer.executor, TestExecutor) assert len(installer.executor.installations) == 1 assert installer.executor.installations[0] == Package( "torch", version, source_type=source_type, source_url=source_url, source_reference=source_reference, ) @pytest.mark.parametrize("lock_version", ("2.0", "2.1")) @pytest.mark.parametrize("env_platform_machine", ["aarch64", "amd64"]) def test_installer_distinguishes_locked_packages_with_same_version_by_source( pool: RepositoryPool, locker: Locker, installed: CustomInstalledRepository, config: Config, repo: Repository, package: ProjectPackage, env_platform_machine: str, lock_version: str, ) -> None: """https://github.com/python-poetry/poetry/issues/8303""" package.add_dependency( Factory.create_dependency( "kivy", { "version": "2.2.1", "markers": "platform_machine == 'aarch64'", "source": "pywheels", }, ) ) package.add_dependency( Factory.create_dependency( "kivy", { "version": "2.2.1", "markers": "platform_machine != 'aarch64'", "source": "PyPI", }, ) ) # Locking finds both the pypi and the pyhweels packages. lock_data: dict[str, Any] = { "package": [ { "name": "kivy", "version": "2.2.1", "optional": False, "files": [], "python-versions": "*", }, { "name": "kivy", "version": "2.2.1", "optional": False, "files": [], "python-versions": "*", "source": { "type": "legacy", "url": "https://www.piwheels.org/simple", "reference": "pywheels", }, }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", }, } if lock_version == "2.1": lock_data["package"][0]["groups"] = ["main"] lock_data["package"][0]["markers"] = "platform_machine != 'aarch64'" lock_data["package"][1]["groups"] = ["main"] lock_data["package"][1]["markers"] = "platform_machine == 'aarch64'" locker.locked(True) locker.mock_lock_data(lock_data) installer = Installer( NullIO(), MockEnv(platform_machine=env_platform_machine), package, locker, pool, config, installed=installed, executor=TestExecutor( MockEnv(platform_machine=env_platform_machine), pool, config, NullIO(), ), ) result = installer.run() assert result == 0 # Results of installation are consistent with the platform requirements. version = "2.2.1" if env_platform_machine == "aarch64": source_type = "legacy" source_url = "https://www.piwheels.org/simple" source_reference = "pywheels" else: source_type = None source_url = None source_reference = None assert isinstance(installer.executor, TestExecutor) assert len(installer.executor.installations) == 1 assert installer.executor.installations[0] == Package( "kivy", version, source_type=source_type, source_url=source_url, source_reference=source_reference, ) @pytest.mark.parametrize("lock_version", ("2.0", "2.1")) @pytest.mark.parametrize("env_platform", ["darwin", "linux"]) def test_explicit_source_dependency_with_direct_origin_dependency( pool: RepositoryPool, locker: Locker, installed: CustomInstalledRepository, config: Config, repo: Repository, package: ProjectPackage, env_platform: str, lock_version: str, ) -> None: """ A dependency with explicit source should not be satisfied by a direct origin dependency even if there is a version match. """ demo_url = ( "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" ) package.add_dependency( Factory.create_dependency( "demo", { "markers": "sys_platform != 'darwin'", "url": demo_url, }, ) ) package.add_dependency( Factory.create_dependency( "demo", { "version": "0.1.0", "markers": "sys_platform == 'darwin'", "source": "repo", }, ) ) # The url demo dependency depends on pendulum. repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("demo", "0.1.0")) # Locking finds both the direct origin and the explicit source packages. lock_data: dict[str, Any] = { "package": [ { "name": "demo", "version": "0.1.0", "optional": False, "files": [], "python-versions": "*", "dependencies": {"pendulum": ">=1.4.4"}, "source": { "type": "url", "url": demo_url, }, }, { "name": "demo", "version": "0.1.0", "optional": False, "files": [], "python-versions": "*", "source": { "type": "legacy", "url": "https://www.demo.org/simple", "reference": "repo", }, }, { "name": "pendulum", "version": "1.4.4", "optional": False, "files": [], "python-versions": "*", }, ], "metadata": { "lock-version": lock_version, "python-versions": "*", "content-hash": "123456789", }, } if lock_version == "2.1": for locked_package in lock_data["package"]: locked_package["groups"] = ["main"] lock_data["package"][0]["markers"] = "sys_platform != 'darwin'" lock_data["package"][1]["markers"] = "sys_platform == 'darwin'" lock_data["package"][2]["markers"] = "sys_platform != 'darwin'" locker.locked(True) locker.mock_lock_data(lock_data) installer = Installer( NullIO(), MockEnv(platform=env_platform), package, locker, pool, config, installed=installed, executor=TestExecutor( MockEnv(platform=env_platform), pool, config, NullIO(), ), ) result = installer.run() assert result == 0 assert isinstance(installer.executor, TestExecutor) if env_platform == "linux": assert set(installer.executor.installations) == { Package("pendulum", "1.4.4"), Package( "demo", "0.1.0", source_type="url", source_url=demo_url, ), } else: assert installer.executor.installations == [ Package( "demo", "0.1.0", source_type="legacy", source_url="https://www.demo.org/simple", source_reference="repo", ) ] ================================================ FILE: tests/installation/test_wheel_installer.py ================================================ from __future__ import annotations import re from pathlib import Path from typing import TYPE_CHECKING import pytest from poetry.core.constraints.version import parse_constraint from poetry.installation.wheel_installer import WheelInstaller from poetry.utils.env import MockEnv if TYPE_CHECKING: from pytest import TempPathFactory from tests.types import FixtureDirGetter @pytest.fixture def env(tmp_path: Path) -> MockEnv: return MockEnv(path=tmp_path) @pytest.fixture(scope="module") def demo_wheel(fixture_dir: FixtureDirGetter) -> Path: return fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") @pytest.fixture(scope="module") def default_installation(tmp_path_factory: TempPathFactory, demo_wheel: Path) -> Path: env = MockEnv(path=tmp_path_factory.mktemp("default_install")) installer = WheelInstaller(env) installer.install(demo_wheel) return Path(env.paths["purelib"]) def test_default_installation_source_dir_content(default_installation: Path) -> None: source_dir = default_installation / "demo" assert source_dir.exists() assert (source_dir / "__init__.py").exists() def test_default_installation_dist_info_dir_content(default_installation: Path) -> None: dist_info_dir = default_installation / "demo-0.1.0.dist-info" assert dist_info_dir.exists() assert (dist_info_dir / "INSTALLER").exists() assert (dist_info_dir / "METADATA").exists() assert (dist_info_dir / "RECORD").exists() assert (dist_info_dir / "WHEEL").exists() def test_installer_file_contains_valid_version(default_installation: Path) -> None: installer_file = default_installation / "demo-0.1.0.dist-info" / "INSTALLER" with open(installer_file, encoding="utf-8") as f: installer_content = f.read() match = re.match(r"Poetry (?P.*)", installer_content) assert match parse_constraint(match.group("version")) # must not raise an error def test_default_installation_no_bytecode(default_installation: Path) -> None: cache_dir = default_installation / "demo" / "__pycache__" assert not cache_dir.exists() @pytest.mark.parametrize("compile", [True, False]) def test_enable_bytecode_compilation( env: MockEnv, demo_wheel: Path, compile: bool ) -> None: installer = WheelInstaller(env) installer.enable_bytecode_compilation(compile) installer.install(demo_wheel) cache_dir = Path(env.paths["purelib"]) / "demo" / "__pycache__" if compile: assert cache_dir.exists() assert list(cache_dir.glob("*.pyc")) assert not list(cache_dir.glob("*.opt-1.pyc")) assert not list(cache_dir.glob("*.opt-2.pyc")) else: assert not cache_dir.exists() ================================================ FILE: tests/integration/__init__.py ================================================ ================================================ FILE: tests/integration/test_utils_vcs_git.py ================================================ from __future__ import annotations import os import uuid from copy import deepcopy from hashlib import sha1 from pathlib import Path from typing import TYPE_CHECKING from typing import TypedDict from urllib.parse import urlparse from urllib.parse import urlunparse import pytest from dulwich.client import HTTPUnauthorized from dulwich.client import get_transport_and_path from dulwich.config import CaseInsensitiveOrderedMultiDict from dulwich.config import ConfigFile from dulwich.refs import Ref from dulwich.repo import Repo from poetry.console.exceptions import PoetryConsoleError from poetry.pyproject.toml import PyProjectTOML from poetry.utils.authenticator import Authenticator from poetry.vcs.git import Git from poetry.vcs.git.backend import GitRefSpec if TYPE_CHECKING: from collections.abc import Iterator from dulwich.client import FetchPackResult from dulwich.client import GitClient from pytest import TempPathFactory from pytest_mock import MockerFixture from tests.conftest import Config # these tests are integration as they rely on an external repository # see `source_url` fixture pytestmark = pytest.mark.integration class GitCloneKwargs(TypedDict): name: str | None branch: str | None tag: str | None revision: str | None source_root: Path | None clean: bool @pytest.fixture(autouse=True) def git_mock() -> None: pass @pytest.fixture(autouse=True) def setup(config: Config) -> None: pass REVISION_TO_VERSION_MAP = { "b6204750a763268e941cec1f05f8986b6c66913e": "0.1.0", # Annotated Tag "18d3ff247d288da701fc7f9ce2ec718388fca266": "0.1.1-alpha.0", "dd07e8d4efb82690e7975b289917a7782fbef29b": "0.2.0-alpha.0", "7263819922b4cd008afbb447f425a562432dad7d": "0.2.0-alpha.1", } BRANCH_TO_REVISION_MAP = {"0.1": "18d3ff247d288da701fc7f9ce2ec718388fca266"} TAG_TO_REVISION_MAP = {"v0.1.0": "b6204750a763268e941cec1f05f8986b6c66913e"} REF_TO_REVISION_MAP = { "branch": BRANCH_TO_REVISION_MAP, "tag": TAG_TO_REVISION_MAP, } @pytest.fixture def use_system_git_client(config: Config) -> None: config.merge({"system-git-client": True}) @pytest.fixture(scope="module") def source_url() -> str: return "https://github.com/python-poetry/test-fixture-vcs-repository.git" @pytest.fixture(scope="module") def source_directory_name(source_url: str) -> str: return Git.get_name_from_source_url(url=source_url) @pytest.fixture(scope="module") def local_repo( tmp_path_factory: TempPathFactory, source_directory_name: str ) -> Iterator[Repo]: with Repo.init( str(tmp_path_factory.mktemp("src") / source_directory_name), mkdir=True ) as repo: yield repo @pytest.fixture(scope="module") def _remote_refs(source_url: str, local_repo: Repo) -> FetchPackResult: client: GitClient path: str client, path = get_transport_and_path(source_url) return client.fetch( path, local_repo, determine_wants=local_repo.object_store.determine_wants_all ) @pytest.fixture def remote_refs(_remote_refs: FetchPackResult) -> FetchPackResult: return deepcopy(_remote_refs) @pytest.fixture(scope="module") def remote_default_ref(_remote_refs: FetchPackResult) -> Ref: ref: Ref = _remote_refs.symrefs[Ref(b"HEAD")] return ref @pytest.fixture(scope="module") def remote_default_branch(remote_default_ref: bytes) -> str: return remote_default_ref.decode("utf-8").replace("refs/heads/", "") # Regression test for https://github.com/python-poetry/poetry/issues/6722 def test_use_system_git_client_from_environment_variables() -> None: os.environ["POETRY_SYSTEM_GIT_CLIENT"] = "true" assert Git.is_using_legacy_client() def test_git_local_info( source_url: str, remote_refs: FetchPackResult, remote_default_ref: Ref ) -> None: with Git.clone(url=source_url) as repo: info = Git.info(repo=repo) assert info.origin == source_url ref = remote_refs.refs[remote_default_ref] assert ref is not None assert info.revision == ref.decode("utf-8") @pytest.mark.parametrize( "specification", [{}, {"revision": "HEAD"}, {"branch": "HEAD"}] ) def test_git_clone_default_branch_head( specification: GitCloneKwargs, source_url: str, remote_refs: FetchPackResult, remote_default_ref: Ref, mocker: MockerFixture, ) -> None: spy = mocker.spy(Git, "_clone") spy_legacy = mocker.spy(Git, "_clone_legacy") with Git.clone(url=source_url, **specification) as repo: assert remote_refs.refs[remote_default_ref] == repo.head() spy_legacy.assert_not_called() spy.assert_called() def test_git_clone_fails_for_non_existent_branch(source_url: str) -> None: branch = uuid.uuid4().hex with pytest.raises(PoetryConsoleError) as e: Git.clone(url=source_url, branch=branch) assert f"Failed to clone {source_url} at '{branch}'" in str(e.value) def test_git_clone_fails_for_non_existent_revision(source_url: str) -> None: revision = sha1(uuid.uuid4().bytes).hexdigest() with pytest.raises(PoetryConsoleError) as e: Git.clone(url=source_url, revision=revision) assert f"Failed to clone {source_url} at '{revision}'" in str(e.value) def assert_version(repo: Repo, expected_revision: str) -> None: version = PyProjectTOML( path=Path(repo.path).joinpath("pyproject.toml") ).poetry_config["version"] revision = Git.get_revision(repo=repo) assert revision == expected_revision assert revision in REVISION_TO_VERSION_MAP assert version == REVISION_TO_VERSION_MAP[revision] def test_git_clone_when_branch_is_ref(source_url: str) -> None: with Git.clone(url=source_url, branch="refs/heads/0.1") as repo: assert_version(repo, BRANCH_TO_REVISION_MAP["0.1"]) @pytest.mark.parametrize("branch", [*BRANCH_TO_REVISION_MAP.keys()]) def test_git_clone_branch( source_url: str, remote_refs: FetchPackResult, branch: str ) -> None: with Git.clone(url=source_url, branch=branch) as repo: assert_version(repo, BRANCH_TO_REVISION_MAP[branch]) @pytest.mark.parametrize("tag", [*TAG_TO_REVISION_MAP.keys()]) def test_git_clone_tag(source_url: str, remote_refs: FetchPackResult, tag: str) -> None: with Git.clone(url=source_url, tag=tag) as repo: assert_version(repo, TAG_TO_REVISION_MAP[tag]) def test_git_clone_multiple_times( source_url: str, remote_refs: FetchPackResult ) -> None: for revision in REVISION_TO_VERSION_MAP: with Git.clone(url=source_url, revision=revision) as repo: assert_version(repo, revision) def test_git_clone_revision_is_branch( source_url: str, remote_refs: FetchPackResult ) -> None: with Git.clone(url=source_url, revision="0.1") as repo: assert_version(repo, BRANCH_TO_REVISION_MAP["0.1"]) def test_git_clone_revision_is_ref( source_url: str, remote_refs: FetchPackResult ) -> None: with Git.clone(url=source_url, revision="refs/heads/0.1") as repo: assert_version(repo, BRANCH_TO_REVISION_MAP["0.1"]) @pytest.mark.parametrize( ("revision", "expected_revision"), [ ("0.1", BRANCH_TO_REVISION_MAP["0.1"]), ("v0.1.0", TAG_TO_REVISION_MAP["v0.1.0"]), *zip(REVISION_TO_VERSION_MAP, REVISION_TO_VERSION_MAP), ], ) def test_git_clone_revision_is_tag( source_url: str, remote_refs: FetchPackResult, revision: str, expected_revision: str ) -> None: with Git.clone(url=source_url, revision=revision) as repo: assert_version(repo, expected_revision) def test_git_clone_clones_submodules(source_url: str) -> None: with Git.clone(url=source_url) as repo: submodule_package_directory = ( Path(repo.path) / "submodules" / "sample-namespace-packages" ) assert submodule_package_directory.exists() assert submodule_package_directory.joinpath("README.md").exists() assert len(list(submodule_package_directory.glob("*"))) > 1 def test_git_clone_clones_submodules_with_relative_urls(source_url: str) -> None: with Git.clone(url=source_url, branch="relative_submodule") as repo: submodule_package_directory = ( Path(repo.path) / "submodules" / "relative-url-submodule" ) assert submodule_package_directory.exists() assert submodule_package_directory.joinpath("README.md").exists() assert len(list(submodule_package_directory.glob("*"))) > 1 def test_git_clone_clones_submodules_with_relative_urls_and_explicit_base( source_url: str, ) -> None: with Git.clone(url=source_url, branch="relative_submodule") as repo: submodule_package_directory = ( Path(repo.path) / "submodules" / "relative-url-submodule-with-base" ) assert submodule_package_directory.exists() assert submodule_package_directory.joinpath("README.md").exists() assert len(list(submodule_package_directory.glob("*"))) > 1 def test_system_git_fallback_on_http_401( mocker: MockerFixture, source_url: str, tmp_path: Path, ) -> None: spy = mocker.spy(Git, "_clone_legacy") mocker.patch.object( Git, "_clone", side_effect=HTTPUnauthorized(None, source_url), ) # use tmp_path for source_root to get a shorter path, # because long paths can cause issues with the system git client on Windows # despite of setting core.longpaths=true with Git.clone(url=source_url, branch="0.1", source_root=tmp_path) as repo: path = Path(repo.path) assert_version(repo, BRANCH_TO_REVISION_MAP["0.1"]) spy.assert_called_with( url="https://github.com/python-poetry/test-fixture-vcs-repository.git", target=path, refspec=GitRefSpec(branch="0.1", revision=None, tag=None, ref=Ref(b"HEAD")), ) spy.assert_called_once() GIT_USERNAME = os.environ.get("POETRY_TEST_INTEGRATION_GIT_USERNAME") GIT_PASSWORD = os.environ.get("POETRY_TEST_INTEGRATION_GIT_PASSWORD") HTTP_AUTH_CREDENTIALS_UNAVAILABLE = not (GIT_USERNAME and GIT_PASSWORD) @pytest.mark.skipif( HTTP_AUTH_CREDENTIALS_UNAVAILABLE, reason="HTTP authentication credentials not available", ) def test_configured_repository_http_auth( mocker: MockerFixture, source_url: str, config: Config ) -> None: from poetry.vcs.git import backend spy_clone_legacy = mocker.spy(Git, "_clone_legacy") spy_get_transport_and_path = mocker.spy(backend, "get_transport_and_path") config.merge( { "repositories": {"git-repo": {"url": source_url}}, "http-basic": { "git-repo": { "username": GIT_USERNAME, "password": GIT_PASSWORD, } }, } ) dummy_git_config = ConfigFile() mocker.patch( "poetry.vcs.git.backend.Repo.get_config_stack", return_value=dummy_git_config, ) mocker.patch( "poetry.vcs.git.backend.get_default_authenticator", return_value=Authenticator(config=config), ) with Git.clone(url=source_url, branch="0.1") as repo: assert_version(repo, BRANCH_TO_REVISION_MAP["0.1"]) spy_clone_legacy.assert_not_called() spy_get_transport_and_path.assert_called_with( location=source_url, config=dummy_git_config, username=GIT_USERNAME, password=GIT_PASSWORD, ) spy_get_transport_and_path.assert_called_once() def test_username_password_parameter_is_not_passed_to_dulwich( mocker: MockerFixture, source_url: str, config: Config ) -> None: from poetry.vcs.git import backend spy_clone = mocker.spy(Git, "_clone") spy_get_transport_and_path = mocker.spy(backend, "get_transport_and_path") dummy_git_config = ConfigFile() mocker.patch( "poetry.vcs.git.backend.Repo.get_config_stack", return_value=dummy_git_config, ) with Git.clone(url=source_url, branch="0.1") as repo: assert_version(repo, BRANCH_TO_REVISION_MAP["0.1"]) spy_clone.assert_called_once() spy_get_transport_and_path.assert_called_with( location=source_url, config=dummy_git_config, username=None, password=None, ) spy_get_transport_and_path.assert_called_once() def test_system_git_called_when_configured( mocker: MockerFixture, source_url: str, use_system_git_client: None, tmp_path: Path ) -> None: spy_legacy = mocker.spy(Git, "_clone_legacy") spy = mocker.spy(Git, "_clone") # use tmp_path for source_root to get a shorter path, # because long paths can cause issues with the system git client on Windows # despite of setting core.longpaths=true with Git.clone(url=source_url, branch="0.1", source_root=tmp_path) as repo: path = Path(repo.path) assert_version(repo, BRANCH_TO_REVISION_MAP["0.1"]) spy.assert_not_called() spy_legacy.assert_called_once() spy_legacy.assert_called_with( url=source_url, target=path, refspec=GitRefSpec(branch="0.1", revision=None, tag=None, ref=Ref(b"HEAD")), ) def test_relative_submodules_with_ssh( source_url: str, tmpdir: Path, mocker: MockerFixture ) -> None: target = tmpdir / "temp" ssh_source_url = urlunparse(urlparse(source_url)._replace(scheme="ssh")) repo_with_unresolved_submodules = Git._clone( url=source_url, refspec=GitRefSpec(branch="relative_submodule"), target=target, ) # construct fake git config values = CaseInsensitiveOrderedMultiDict.make( {b"url": ssh_source_url.encode("utf-8")} ) fake_config = ConfigFile({(b"remote", b"origin"): values}) # trick Git into thinking remote.origin is an ssh url mock_get_config = mocker.patch.object(repo_with_unresolved_submodules, "get_config") mock_get_config.return_value = fake_config submodules = Git._get_submodules(repo_with_unresolved_submodules) assert [s.url for s in submodules] == [ "https://github.com/pypa/sample-namespace-packages.git", "ssh://github.com/python-poetry/test-fixture-vcs-repository.git", "ssh://github.com/python-poetry/test-fixture-vcs-repository.git", ] ================================================ FILE: tests/json/__init__.py ================================================ ================================================ FILE: tests/json/fixtures/build_constraints.toml ================================================ [project] name = "build-constraints" version = "0.1.0" [tool.poetry.build-constraints] Legacy-Lib = { setuptools = "<75" } no-constraints = {} [tool.poetry.build-constraints.c-ext-lib] Cython = { version = "<3.1", source = "pypi" } setuptools = [ { version = ">=60,<75", python = "<3.9" }, { version = ">=75", python = ">=3.8" } ] ================================================ FILE: tests/json/fixtures/self_invalid_plugin.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Your Name "] [tool.poetry.requires-plugins] foo = 5 ================================================ FILE: tests/json/fixtures/self_invalid_version.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Your Name "] requires-poetry = 2 ================================================ FILE: tests/json/fixtures/self_valid.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Your Name "] requires-poetry = ">=2.0" [tool.poetry.requires-plugins] foo = ">=1.0" ================================================ FILE: tests/json/fixtures/source/complete_invalid_priority.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.10" [[tool.poetry.source]] name = "pypi-simple" url = "https://pypi.org/simple/" priority = "arbitrary" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/json/fixtures/source/complete_invalid_url.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.10" [[tool.poetry.source]] name = "pypi-simple" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/json/fixtures/source/complete_valid.toml ================================================ [tool.poetry] name = "foobar" version = "0.1.0" description = "" authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.10" [[tool.poetry.source]] name = "pypi-simple" url = "https://pypi.org/simple/" priority = "explicit" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/json/test_schema.py ================================================ from __future__ import annotations import json from importlib.resources import files from pathlib import Path from typing import Any from poetry.factory import Factory from poetry.toml import TOMLFile SCHEMA_FILE = files("poetry.json") / "schemas" / "poetry.json" FIXTURE_DIR = Path(__file__).parent / "fixtures" SOURCE_FIXTURE_DIR = FIXTURE_DIR / "source" def test_pyproject_toml_valid() -> None: toml: dict[str, Any] = TOMLFile(SOURCE_FIXTURE_DIR / "complete_valid.toml").read() assert Factory.validate(toml) == {"errors": [], "warnings": []} def test_pyproject_toml_invalid_priority() -> None: toml: dict[str, Any] = TOMLFile( SOURCE_FIXTURE_DIR / "complete_invalid_priority.toml" ).read() assert Factory.validate(toml) == { "errors": [ "tool.poetry.source[0].priority must be one of ['primary'," " 'supplemental', 'explicit']" ], "warnings": [], } def test_self_valid() -> None: toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_valid.toml").read() assert Factory.validate(toml) == {"errors": [], "warnings": []} def test_self_invalid_version() -> None: toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_invalid_version.toml").read() assert Factory.validate(toml) == { "errors": ["tool.poetry.requires-poetry must be string"], "warnings": [], } def test_self_invalid_plugin() -> None: toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_invalid_plugin.toml").read() assert Factory.validate(toml) == { "errors": [ "tool.poetry.requires-plugins.foo must be valid exactly by one definition" " (0 matches found)" ], "warnings": [], } def test_build_constraints() -> None: toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "build_constraints.toml").read() assert Factory.validate(toml) == {"errors": [], "warnings": []} def test_dependencies_is_consistent_to_poetry_core_schema() -> None: with SCHEMA_FILE.open(encoding="utf-8") as f: schema = json.load(f) dependency_definitions = { key: value for key, value in schema["definitions"].items() if "depend" in key } with (files("poetry.core") / "json" / "schemas" / "poetry-schema.json").open( encoding="utf-8" ) as f: core_schema = json.load(f) core_dependency_definitions = { key: value for key, value in core_schema["definitions"].items() if "depend" in key } assert dependency_definitions == core_dependency_definitions ================================================ FILE: tests/masonry/builders/__init__.py ================================================ ================================================ FILE: tests/masonry/builders/fixtures/excluded_subpackage/README.rst ================================================ My Package ========== ================================================ FILE: tests/masonry/builders/fixtures/excluded_subpackage/example/__init__.py ================================================ from __future__ import annotations __version__ = "0.1.0" ================================================ FILE: tests/masonry/builders/fixtures/excluded_subpackage/example/test/__init__.py ================================================ ================================================ FILE: tests/masonry/builders/fixtures/excluded_subpackage/example/test/excluded.py ================================================ from __future__ import annotations from tests.masonry.builders.fixtures.excluded_subpackage.example import __version__ def test_version(): assert __version__ == "0.1.0" ================================================ FILE: tests/masonry/builders/fixtures/excluded_subpackage/pyproject.toml ================================================ [tool.poetry] name = "example" version = "0.1.0" description = "" authors = ["Sébastien Eustace "] exclude = [ "**/test/**/*", ] [tool.poetry.dependencies] python = "^3.6" [tool.poetry.group.dev.dependencies] pytest = "^3.0" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" ================================================ FILE: tests/masonry/builders/test_editable_builder.py ================================================ from __future__ import annotations import csv import json import os import shutil from pathlib import Path from typing import TYPE_CHECKING import pytest from cleo.io.null_io import NullIO from deepdiff.diff import DeepDiff from poetry.core.constraints.version import Version from poetry.core.masonry.metadata import Metadata from poetry.core.packages.package import Package from poetry.factory import Factory from poetry.masonry.builders.editable import EditableBuilder from poetry.repositories.installed_repository import InstalledRepository from poetry.utils._compat import getencoding from poetry.utils.env import EnvCommandError from poetry.utils.env import EnvManager from poetry.utils.env import MockEnv from poetry.utils.env import VirtualEnv from poetry.utils.env import ephemeral_environment if TYPE_CHECKING: from collections.abc import Iterator from pytest_mock import MockerFixture from poetry.poetry import Poetry from tests.types import FixtureDirGetter @pytest.fixture() def simple_poetry(fixture_dir: FixtureDirGetter) -> Poetry: poetry = Factory().create_poetry(fixture_dir("simple_project")) return poetry @pytest.fixture() def project_with_include(fixture_dir: FixtureDirGetter) -> Poetry: poetry = Factory().create_poetry(fixture_dir("with-include")) return poetry @pytest.fixture() def extended_poetry(fixture_dir: FixtureDirGetter) -> Poetry: poetry = Factory().create_poetry(fixture_dir("extended_project")) return poetry @pytest.fixture() def extended_without_setup_poetry(fixture_dir: FixtureDirGetter) -> Poetry: poetry = Factory().create_poetry(fixture_dir("extended_project_without_setup")) return poetry @pytest.fixture def with_multiple_readme_files(fixture_dir: FixtureDirGetter) -> Poetry: poetry = Factory().create_poetry(fixture_dir("with_multiple_readme_files")) return poetry @pytest.fixture() def env_manager(simple_poetry: Poetry) -> EnvManager: return EnvManager(simple_poetry) @pytest.fixture def tmp_venv(tmp_path: Path, env_manager: EnvManager) -> Iterator[VirtualEnv]: venv_path = tmp_path / "venv" env_manager.build_venv(venv_path) venv = VirtualEnv(venv_path) yield venv shutil.rmtree(str(venv.path)) @pytest.fixture() def bad_scripts_no_colon(fixture_dir: FixtureDirGetter) -> Poetry: poetry = Factory().create_poetry(fixture_dir("bad_scripts_project/no_colon")) return poetry @pytest.fixture() def bad_scripts_too_many_colon(fixture_dir: FixtureDirGetter) -> Poetry: poetry = Factory().create_poetry(fixture_dir("bad_scripts_project/too_many_colon")) return poetry def expected_metadata_version() -> str: # Get the metadata version that we should expect: which is poetry-core's default # value. metadata = Metadata() return metadata.metadata_version def expected_python_classifiers(min_version: str | None = None) -> str: if min_version: min_version_tuple = tuple(map(int, min_version.split("."))) relevant_versions = set() for version in Package.AVAILABLE_PYTHONS: version_tuple = tuple(map(int, version.split("."))) if version_tuple >= min_version_tuple[: len(version_tuple)]: relevant_versions.add(version) else: relevant_versions = Package.AVAILABLE_PYTHONS return "\n".join( f"Classifier: Programming Language :: Python :: {version}" for version in sorted( relevant_versions, key=lambda x: tuple(map(int, x.split("."))) ) ) @pytest.mark.parametrize("project", ("simple_project", "simple_project_legacy")) def test_builder_installs_proper_files_for_standard_packages( project: str, simple_poetry: Poetry, tmp_path: Path, fixture_dir: FixtureDirGetter, ) -> None: simple_poetry = Factory().create_poetry(fixture_dir(project)) env_manager = EnvManager(simple_poetry) venv_path = tmp_path / "venv" env_manager.build_venv(venv_path) tmp_venv = VirtualEnv(venv_path) builder = EditableBuilder(simple_poetry, tmp_venv, NullIO()) builder.build() assert tmp_venv._bin_dir.joinpath("foo").exists() pth_file = Path("simple_project.pth") assert tmp_venv.site_packages.exists(pth_file) assert ( simple_poetry.file.path.parent.resolve().as_posix() == tmp_venv.site_packages.find(pth_file)[0] .read_text(encoding="utf-8") .strip(os.linesep) ) dist_info = Path("simple_project-1.2.3.dist-info") assert tmp_venv.site_packages.exists(dist_info) dist_info = tmp_venv.site_packages.find(dist_info)[0] assert dist_info.joinpath("entry_points.txt").exists() assert dist_info.joinpath("WHEEL").exists() assert dist_info.joinpath("METADATA").exists() assert dist_info.joinpath("licenses/LICENSE").exists() assert dist_info.joinpath("INSTALLER").exists() assert dist_info.joinpath("RECORD").exists() assert dist_info.joinpath("direct_url.json").exists() assert not DeepDiff( { "dir_info": {"editable": True}, "url": simple_poetry.file.path.parent.as_uri(), }, json.loads(dist_info.joinpath("direct_url.json").read_text(encoding="utf-8")), ) assert dist_info.joinpath("INSTALLER").read_text(encoding="utf-8") == "poetry" assert ( dist_info.joinpath("entry_points.txt").read_text(encoding="utf-8") == "[console_scripts]\nbaz=bar:baz.boom.bim\nfoo=foo:bar\n" "fox=fuz.foo:bar.baz\n\n" ) metadata = f"""\ Metadata-Version: {expected_metadata_version()} Name: simple-project Version: 1.2.3 Summary: Some description. License: MIT License-File: LICENSE Keywords: packaging,dependency,poetry Author: Sébastien Eustace Author-email: sebastien@eustace.io Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License {expected_python_classifiers()} Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Project-URL: Documentation, https://python-poetry.org/docs Project-URL: Homepage, https://python-poetry.org Project-URL: Repository, https://github.com/python-poetry/poetry Description-Content-Type: text/x-rst My Package ========== """ if project == "simple_project": metadata = metadata.replace("License:", "License-Expression:").replace( "Classifier: License :: OSI Approved :: MIT License\n", "" ) assert dist_info.joinpath("METADATA").read_text(encoding="utf-8") == metadata with open(dist_info.joinpath("RECORD"), encoding="utf-8", newline="") as f: reader = csv.reader(f) records = list(reader) assert all(len(row) == 3 for row in records) record_entries = {row[0] for row in records} pth_file = Path("simple_project.pth") assert tmp_venv.site_packages.exists(pth_file) assert str(tmp_venv.site_packages.find(pth_file)[0]) in record_entries assert str(tmp_venv._bin_dir.joinpath("foo")) in record_entries assert str(tmp_venv._bin_dir.joinpath("baz")) in record_entries assert str(dist_info.joinpath("entry_points.txt")) in record_entries assert str(dist_info.joinpath("WHEEL")) in record_entries assert str(dist_info.joinpath("METADATA")) in record_entries assert str(dist_info.joinpath("licenses/LICENSE")) in record_entries assert str(dist_info.joinpath("INSTALLER")) in record_entries assert str(dist_info.joinpath("RECORD")) in record_entries assert str(dist_info.joinpath("direct_url.json")) in record_entries baz_script = f"""\ #!{tmp_venv.python} import sys from bar import baz if __name__ == '__main__': sys.exit(baz.boom.bim()) """ assert tmp_venv._bin_dir.joinpath("baz").read_text(encoding="utf-8") == baz_script foo_script = f"""\ #!{tmp_venv.python} import sys from foo import bar if __name__ == '__main__': sys.exit(bar()) """ assert tmp_venv._bin_dir.joinpath("foo").read_text(encoding="utf-8") == foo_script fox_script = f"""\ #!{tmp_venv.python} import sys from fuz.foo import bar if __name__ == '__main__': sys.exit(bar.baz()) """ assert tmp_venv._bin_dir.joinpath("fox").read_text(encoding="utf-8") == fox_script def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts( mocker: MockerFixture, extended_poetry: Poetry, tmp_path: Path ) -> None: pip_install = mocker.patch("poetry.masonry.builders.editable.pip_install") env = MockEnv(path=tmp_path / "foo") builder = EditableBuilder(extended_poetry, env, NullIO()) builder.build() pip_install.assert_called_once_with( extended_poetry.pyproject.file.path.parent, env, upgrade=True, editable=True ) assert env.executed == [] @pytest.mark.network def test_builder_setup_generation_runs_with_pip_editable( fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: # create an isolated copy of the project fixture = fixture_dir("extended_project") extended_project = tmp_path / "extended_project" shutil.copytree(fixture, extended_project) assert extended_project.exists() poetry = Factory().create_poetry(extended_project) # we need a venv with pip and setuptools since we are verifying setup.py builds with ephemeral_environment(flags={"no-pip": False}) as venv: builder = EditableBuilder(poetry, venv, NullIO()) builder.build() # is the package installed? repository = InstalledRepository.load(venv) package = repository.package("extended-project", Version.parse("1.2.3")) assert package.name == "extended-project" # check for the module built by build.py try: output = venv.run_python_script( "from extended_project import built; print(built.__file__)" ).strip() except EnvCommandError: pytest.fail("Unable to import built module") else: built_py = Path(output).resolve() expected = extended_project / "extended_project" / "built.py" # ensure the package was installed as editable assert built_py == expected.resolve() def test_builder_installs_proper_files_when_packages_configured( project_with_include: Poetry, tmp_venv: VirtualEnv ) -> None: builder = EditableBuilder(project_with_include, tmp_venv, NullIO()) builder.build() pth_file = Path("with_include.pth") assert tmp_venv.site_packages.exists(pth_file) pth_file = tmp_venv.site_packages.find(pth_file)[0] paths = set() with pth_file.open(encoding=getencoding()) as f: for line in f.readlines(): line = line.strip(os.linesep) if line: paths.add(line) project_root = project_with_include.file.path.parent.resolve() expected = {project_root.as_posix(), project_root.joinpath("src").as_posix()} assert paths.issubset(expected) assert len(paths) == len(expected) def test_builder_generates_proper_metadata_when_multiple_readme_files( with_multiple_readme_files: Poetry, tmp_venv: VirtualEnv ) -> None: builder = EditableBuilder(with_multiple_readme_files, tmp_venv, NullIO()) builder.build() dist_info = Path("my_package-0.1.dist-info") assert tmp_venv.site_packages.exists(dist_info) dist_info = tmp_venv.site_packages.find(dist_info)[0] assert dist_info.joinpath("METADATA").exists() metadata = f"""\ Metadata-Version: {expected_metadata_version()} Name: my-package Version: 0.1 Summary: Some description. License: MIT Author: Your Name Author-email: you@example.com Requires-Python: >=3.7,<4.0 Classifier: License :: OSI Approved :: MIT License {expected_python_classifiers("3.7")} Project-URL: Homepage, https://python-poetry.org Description-Content-Type: text/x-rst Single Python ============= Changelog ========= """ assert dist_info.joinpath("METADATA").read_text(encoding="utf-8") == metadata def test_builder_should_execute_build_scripts( mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_path: Path ) -> None: env = MockEnv(path=tmp_path / "foo") mocker.patch( "poetry.masonry.builders.editable.build_environment" ).return_value.__enter__.return_value = env builder = EditableBuilder(extended_without_setup_poetry, env, NullIO()) builder.build() assert [ ["python", str(extended_without_setup_poetry.file.path.parent / "build.py")] ] == env.executed def test_builder_catches_bad_scripts_no_colon( bad_scripts_no_colon: Poetry, tmp_venv: VirtualEnv ) -> None: builder = EditableBuilder(bad_scripts_no_colon, tmp_venv, NullIO()) with pytest.raises(ValueError, match=r"Bad script.*") as e: builder.build() msg = str(e.value) # We should print out the problematic script entry assert "bar.bin.foo" in msg # and some hint about what to do assert "Hint:" in msg assert 'foo = "bar.bin.foo:main"' in msg def test_builder_catches_bad_scripts_too_many_colon( bad_scripts_too_many_colon: Poetry, tmp_venv: VirtualEnv ) -> None: builder = EditableBuilder(bad_scripts_too_many_colon, tmp_venv, NullIO()) with pytest.raises(ValueError, match=r"Bad script.*") as e: builder.build() msg = str(e.value) # We should print out the problematic script entry assert "foo::bar" in msg # and some hint about what is wrong assert "Too many" in msg ================================================ FILE: tests/mixology/__init__.py ================================================ ================================================ FILE: tests/mixology/helpers.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.core.packages.package import Package from poetry.factory import Factory from poetry.mixology.failure import SolveFailureError from poetry.mixology.version_solver import VersionSolver if TYPE_CHECKING: from collections.abc import Mapping from packaging.utils import NormalizedName from poetry.core.factory import DependencyConstraint from poetry.core.packages.project_package import ProjectPackage from poetry.mixology.result import SolverResult from poetry.repositories import Repository from tests.mixology.version_solver.conftest import Provider def add_to_repo( repository: Repository, name: str, version: str, deps: Mapping[str, DependencyConstraint] | None = None, python: str | None = None, yanked: bool = False, ) -> None: package = Package(name, version, yanked=yanked) if python: package.python_versions = python if deps: for dep_name, dep_constraint in deps.items(): package.add_dependency(Factory.create_dependency(dep_name, dep_constraint)) repository.add_package(package) def check_solver_result( root: ProjectPackage, provider: Provider, result: dict[str, str] | None = None, error: str | None = None, tries: int | None = None, use_latest: list[NormalizedName] | None = None, ) -> SolverResult | None: solver = VersionSolver(root, provider) with provider.use_latest_for(use_latest or []): try: solution = solver.solve() except SolveFailureError as e: if error: assert str(e) == error if tries is not None: assert solver.solution.attempted_solutions == tries return None except AssertionError as e: if error: assert str(e) == error return None raise packages = {} for package in solution.packages: packages[package.name] = str(package.version) assert packages == result if tries is not None: assert solution.attempted_solutions == tries return solution ================================================ FILE: tests/mixology/test_incompatibility.py ================================================ from __future__ import annotations import pytest from poetry.core.packages.dependency import Dependency from poetry.core.packages.url_dependency import URLDependency from poetry.mixology.incompatibility import Incompatibility from poetry.mixology.incompatibility_cause import DependencyCauseError from poetry.mixology.term import Term def get_url_dependency(name: str, url: str, version: str) -> URLDependency: dependency = URLDependency(name, url) dependency.constraint = version return dependency @pytest.mark.parametrize( ("dependency1", "dependency2", "expected"), [ ( Dependency("foo", "1.0"), Dependency("bar", "2.0"), "foo (1.0) depends on bar (2.0)", ), ( Dependency("foo", "1.0"), Dependency("bar", "^1.0"), "foo (1.0) depends on bar (^1.0)", ), ( Dependency("foo", "1.0"), get_url_dependency("bar", "https://example.com/bar.whl", "1.1"), "foo (1.0) depends on bar (1.1) @ https://example.com/bar.whl", ), ( Dependency("foo", "1.0", extras=["bar"]), Dependency("foo", "1.0"), "foo[bar] (1.0) depends on foo (1.0)", ), ], ) def test_str_dependency_cause( dependency1: Dependency, dependency2: Dependency, expected: str ) -> None: incompatibility = Incompatibility( [Term(dependency1, True), Term(dependency2, False)], DependencyCauseError() ) assert str(incompatibility) == expected ================================================ FILE: tests/mixology/version_solver/__init__.py ================================================ from __future__ import annotations import pytest pytest.register_assert_rewrite("tests.mixology.helpers") ================================================ FILE: tests/mixology/version_solver/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.io.null_io import NullIO from poetry.core.packages.project_package import ProjectPackage from poetry.puzzle.provider import Provider as BaseProvider from poetry.repositories import Repository from poetry.repositories import RepositoryPool if TYPE_CHECKING: from tests.helpers import TestRepository class Provider(BaseProvider): def set_package_python_versions(self, python_versions: str) -> None: self._package.python_versions = python_versions self._python_constraint = self._package.python_constraint @pytest.fixture def repo() -> Repository: return Repository("repo") @pytest.fixture def pool(repo: TestRepository) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(repo) return pool @pytest.fixture def root() -> ProjectPackage: return ProjectPackage("myapp", "0.0.0") @pytest.fixture def provider(pool: RepositoryPool, root: ProjectPackage) -> Provider: return Provider(root, pool, NullIO()) ================================================ FILE: tests/mixology/version_solver/test_backtracking.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.factory import Factory from tests.mixology.helpers import add_to_repo from tests.mixology.helpers import check_solver_result if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage from poetry.repositories import Repository from tests.mixology.version_solver.conftest import Provider def test_circular_dependency_on_older_version( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("a", ">=1.0.0")) add_to_repo(repo, "a", "1.0.0") add_to_repo(repo, "a", "2.0.0", deps={"b": "1.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"a": "1.0.0"}) check_solver_result(root, provider, {"a": "1.0.0"}, tries=2) def test_diamond_dependency_graph( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "2.0.0", deps={"c": "^1.0.0"}) add_to_repo(repo, "a", "1.0.0") add_to_repo(repo, "b", "2.0.0", deps={"c": "^3.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"c": "^2.0.0"}) add_to_repo(repo, "c", "3.0.0") add_to_repo(repo, "c", "2.0.0") add_to_repo(repo, "c", "1.0.0") check_solver_result(root, provider, {"a": "1.0.0", "b": "2.0.0", "c": "3.0.0"}) def test_backjumps_after_partial_satisfier( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: # c 2.0.0 is incompatible with y 2.0.0 because it requires x 1.0.0, but that # requirement only exists because of both a and b. The solver should be able # to deduce c 2.0.0's incompatibility and select c 1.0.0 instead. root.add_dependency(Factory.create_dependency("c", "*")) root.add_dependency(Factory.create_dependency("y", "^2.0.0")) add_to_repo(repo, "a", "1.0.0", deps={"x": ">=1.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"x": "<2.0.0"}) add_to_repo(repo, "c", "1.0.0") add_to_repo(repo, "c", "2.0.0", deps={"a": "*", "b": "*"}) add_to_repo(repo, "x", "0.0.0") add_to_repo(repo, "x", "1.0.0", deps={"y": "1.0.0"}) add_to_repo(repo, "x", "2.0.0") add_to_repo(repo, "y", "1.0.0") add_to_repo(repo, "y", "2.0.0") check_solver_result(root, provider, {"c": "1.0.0", "y": "2.0.0"}, tries=4) def test_rolls_back_leaf_versions_first( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: # The latest versions of a and b disagree on c. An older version of either # will resolve the problem. This test validates that b, which is farther # in the dependency graph from myapp is downgraded first. root.add_dependency(Factory.create_dependency("a", "*")) add_to_repo(repo, "a", "1.0.0", deps={"b": "*"}) add_to_repo(repo, "a", "2.0.0", deps={"b": "*", "c": "2.0.0"}) add_to_repo(repo, "b", "1.0.0") add_to_repo(repo, "b", "2.0.0", deps={"c": "1.0.0"}) add_to_repo(repo, "c", "1.0.0") add_to_repo(repo, "c", "2.0.0") check_solver_result(root, provider, {"a": "2.0.0", "b": "1.0.0", "c": "2.0.0"}) def test_simple_transitive( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: # Only one version of baz, so foo and bar will have to downgrade # until they reach it root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "foo", "2.0.0", deps={"bar": "2.0.0"}) add_to_repo(repo, "foo", "3.0.0", deps={"bar": "3.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"baz": "*"}) add_to_repo(repo, "bar", "2.0.0", deps={"baz": "2.0.0"}) add_to_repo(repo, "bar", "3.0.0", deps={"baz": "3.0.0"}) add_to_repo(repo, "baz", "1.0.0") check_solver_result( root, provider, {"foo": "1.0.0", "bar": "1.0.0", "baz": "1.0.0"}, tries=3 ) def test_backjump_to_nearer_unsatisfied_package( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: # This ensures it doesn't exhaustively search all versions of b when it's # a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the problem. We # make sure b has more versions than a so that the solver tries a first # since it sorts sibling dependencies by number of versions. root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "1.0.0", deps={"c": "1.0.0"}) add_to_repo(repo, "a", "2.0.0", deps={"c": "2.0.0-1"}) add_to_repo(repo, "b", "1.0.0") add_to_repo(repo, "b", "2.0.0") add_to_repo(repo, "b", "3.0.0") add_to_repo(repo, "c", "1.0.0") check_solver_result( root, provider, {"a": "1.0.0", "b": "3.0.0", "c": "1.0.0"}, tries=2 ) def test_backjump_past_failed_package_on_disjoint_constraint( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("foo", ">2.0.0")) add_to_repo(repo, "a", "1.0.0", deps={"foo": "*"}) # ok add_to_repo( repo, "a", "2.0.0", deps={"foo": "<1.0.0"} ) # disjoint with myapp's constraint on foo add_to_repo(repo, "foo", "2.0.0") add_to_repo(repo, "foo", "2.0.1") add_to_repo(repo, "foo", "2.0.2") add_to_repo(repo, "foo", "2.0.3") add_to_repo(repo, "foo", "2.0.4") check_solver_result(root, provider, {"a": "1.0.0", "foo": "2.0.4"}) def test_backtracking_performance_level_1( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: """ This test takes quite long if an unfavorable heuristics is chosen to select the next package to resolve. B depends on A, but does not support the latest version of A. B has a lot more versions than A. Test for boto3/botocore vs. urllib3 issue in its simple form. """ root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "1") add_to_repo(repo, "a", "2") b_max = 500 for i in range(1, b_max + 1): add_to_repo(repo, "b", str(i), deps={"a": "<=1"}) check_solver_result(root, provider, {"a": "1", "b": str(b_max)}) def test_backtracking_performance_level_2( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: """ Similar to test_backtracking_performance_level_1, but with one more level of dependencies. C depends on B depends on A, but B does not support the latest version of A. The root dependency only requires A and C so there is no direct dependency between these two. B and C have a lot more versions than A. Test for boto3/botocore vs. urllib3 issue in its more complex form. """ root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("c", "*")) add_to_repo(repo, "a", "1") add_to_repo(repo, "a", "2") bc_max = 500 for i in range(1, bc_max + 1): add_to_repo(repo, "b", str(i), deps={"a": "<=1"}) for i in range(1, bc_max + 1): add_to_repo(repo, "c", str(i), deps={"b": f"<={i}"}) check_solver_result(root, provider, {"a": "1", "b": str(bc_max), "c": str(bc_max)}) ================================================ FILE: tests/mixology/version_solver/test_basic_graph.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.factory import Factory from tests.mixology.helpers import add_to_repo from tests.mixology.helpers import check_solver_result if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage from poetry.repositories import Repository from tests.mixology.version_solver.conftest import Provider def test_simple_dependencies( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("a", "1.0.0")) root.add_dependency(Factory.create_dependency("b", "1.0.0")) add_to_repo(repo, "a", "1.0.0", deps={"aa": "1.0.0", "ab": "1.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"ba": "1.0.0", "bb": "1.0.0"}) add_to_repo(repo, "aa", "1.0.0") add_to_repo(repo, "ab", "1.0.0") add_to_repo(repo, "ba", "1.0.0") add_to_repo(repo, "bb", "1.0.0") check_solver_result( root, provider, { "a": "1.0.0", "aa": "1.0.0", "ab": "1.0.0", "b": "1.0.0", "ba": "1.0.0", "bb": "1.0.0", }, ) def test_shared_dependencies_with_overlapping_constraints( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("a", "1.0.0")) root.add_dependency(Factory.create_dependency("b", "1.0.0")) add_to_repo(repo, "a", "1.0.0", deps={"shared": ">=2.0.0 <4.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"shared": ">=3.0.0 <5.0.0"}) add_to_repo(repo, "shared", "2.0.0") add_to_repo(repo, "shared", "3.0.0") add_to_repo(repo, "shared", "3.6.9") add_to_repo(repo, "shared", "4.0.0") add_to_repo(repo, "shared", "5.0.0") check_solver_result(root, provider, {"a": "1.0.0", "b": "1.0.0", "shared": "3.6.9"}) def test_shared_dependency_where_dependent_version_affects_other_dependencies( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("foo", "<=1.0.2")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) add_to_repo(repo, "foo", "1.0.0") add_to_repo(repo, "foo", "1.0.1", deps={"bang": "1.0.0"}) add_to_repo(repo, "foo", "1.0.2", deps={"whoop": "1.0.0"}) add_to_repo(repo, "foo", "1.0.3", deps={"zoop": "1.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"foo": "<=1.0.1"}) add_to_repo(repo, "bang", "1.0.0") add_to_repo(repo, "whoop", "1.0.0") add_to_repo(repo, "zoop", "1.0.0") check_solver_result( root, provider, {"foo": "1.0.1", "bar": "1.0.0", "bang": "1.0.0"} ) def test_circular_dependency( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("foo", "1.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"foo": "1.0.0"}) check_solver_result(root, provider, {"foo": "1.0.0", "bar": "1.0.0"}) @pytest.mark.parametrize( "constraint, versions, yanked_versions, expected", [ (">=1", ["1", "2"], [], "2"), (">=1", ["1", "2"], ["2"], "1"), (">=1", ["1", "2", "3"], ["2"], "3"), (">=1", ["1", "2", "3"], ["2", "3"], "1"), (">1", ["1", "2"], ["2"], "error"), (">1", ["2"], ["2"], "error"), (">=2", ["2"], ["2"], "error"), ("==2", ["2"], ["2"], "2"), ("==2", ["2", "2+local"], [], "2+local"), ("==2", ["2", "2+local"], ["2+local"], "2"), ], ) def test_yanked_release( root: ProjectPackage, provider: Provider, repo: Repository, constraint: str, versions: list[str], yanked_versions: list[str], expected: str, ) -> None: root.add_dependency(Factory.create_dependency("foo", constraint)) for version in versions: add_to_repo(repo, "foo", version, yanked=version in yanked_versions) if expected == "error": result = None error = ( f"Because myapp depends on foo ({constraint}) which doesn't match any " "versions, version solving failed." ) else: result = {"foo": expected} error = None check_solver_result(root, provider, result, error) ================================================ FILE: tests/mixology/version_solver/test_dependency_cache.py ================================================ from __future__ import annotations from copy import deepcopy from typing import TYPE_CHECKING from unittest import mock from poetry.factory import Factory from poetry.mixology.version_solver import DependencyCache from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.mixology.helpers import add_to_repo if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage from poetry.repositories import Repository from tests.mixology.version_solver.conftest import Provider def test_solver_dependency_cache_respects_source_type( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: dependency_pypi = Factory.create_dependency("demo", ">=0.1.0") dependency_git = Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git"}, groups=["dev"] ) root.add_dependency(dependency_pypi) root.add_dependency(dependency_git) add_to_repo(repo, "demo", "1.0.0") cache = DependencyCache(provider) cache._search_for_cached.cache_clear() # ensure cache was never hit for both calls cache.search_for(dependency_pypi, 0) cache.search_for(dependency_git, 0) assert not cache._search_for_cached.cache_info().hits # increase test coverage by searching for copies # (when searching for the exact same object, __eq__ is never called) packages_pypi = cache.search_for(deepcopy(dependency_pypi), 0) packages_git = cache.search_for(deepcopy(dependency_git), 0) assert cache._search_for_cached.cache_info().hits == 2 assert cache._search_for_cached.cache_info().currsize == 2 assert len(packages_pypi) == len(packages_git) == 1 assert packages_pypi != packages_git package_pypi = packages_pypi[0] package_git = packages_git[0] assert package_pypi.package.name == dependency_pypi.name assert package_pypi.package.version.text == "1.0.0" assert package_git.package.name == dependency_git.name assert package_git.package.version.text == "0.1.2" assert package_git.package.source_type == dependency_git.source_type assert package_git.package.source_url == dependency_git.source_url assert package_git.package.source_resolved_reference == MOCK_DEFAULT_GIT_REVISION def test_solver_dependency_cache_pulls_from_prior_level_cache( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: dependency_pypi = Factory.create_dependency("demo", ">=0.1.0") dependency_pypi_constrained = Factory.create_dependency("demo", ">=0.1.0,<2.0.0") root.add_dependency(dependency_pypi) root.add_dependency(dependency_pypi_constrained) add_to_repo(repo, "demo", "1.0.0") wrapped_provider = mock.Mock(wraps=provider) cache = DependencyCache(wrapped_provider) cache._search_for_cached.cache_clear() # On first call, provider.search_for() should be called and the cache # populated. cache.search_for(dependency_pypi, 0) assert len(wrapped_provider.search_for.mock_calls) == 1 assert ("demo", None, None, None, None) in cache._cache assert ("demo", None, None, None, None) in cache._cached_dependencies_by_level[0] assert cache._search_for_cached.cache_info().hits == 0 assert cache._search_for_cached.cache_info().misses == 1 # On second call at level 1, neither provider.search_for() nor # cache._search_for_cached() should have been called again, and the cache # should remain the same. cache.search_for(dependency_pypi, 1) assert len(wrapped_provider.search_for.mock_calls) == 1 assert ("demo", None, None, None, None) in cache._cache assert ("demo", None, None, None, None) in cache._cached_dependencies_by_level[0] assert set(cache._cached_dependencies_by_level.keys()) == {0} assert cache._search_for_cached.cache_info().hits == 1 assert cache._search_for_cached.cache_info().misses == 1 # On third call at level 2 with an updated constraint for the `demo` # package should not call provider.search_for(), but should call # cache._search_for_cached() and update the cache. cache.search_for(dependency_pypi_constrained, 2) assert len(wrapped_provider.search_for.mock_calls) == 1 assert ("demo", None, None, None, None) in cache._cache assert ("demo", None, None, None, None) in cache._cached_dependencies_by_level[0] assert ("demo", None, None, None, None) in cache._cached_dependencies_by_level[2] assert set(cache._cached_dependencies_by_level.keys()) == {0, 2} assert cache._search_for_cached.cache_info().hits == 1 assert cache._search_for_cached.cache_info().misses == 2 # Clearing the level 2 and level 1 caches should invalidate the lru_cache # on cache.search_for and wipe out the level 2 cache while preserving the # level 0 cache. cache.clear_level(2) cache.clear_level(1) cache.search_for(dependency_pypi, 0) assert len(wrapped_provider.search_for.mock_calls) == 1 assert ("demo", None, None, None, None) in cache._cache assert ("demo", None, None, None, None) in cache._cached_dependencies_by_level[0] assert set(cache._cached_dependencies_by_level.keys()) == {0} assert cache._search_for_cached.cache_info().hits == 0 assert cache._search_for_cached.cache_info().misses == 1 def test_solver_dependency_cache_respects_subdirectories( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: dependency_one = Factory.create_dependency( "one", { "git": "https://github.com/demo/subdirectories.git", "subdirectory": "one", "platform": "linux", }, ) dependency_one_copy = Factory.create_dependency( "one", { "git": "https://github.com/demo/subdirectories.git", "subdirectory": "one-copy", "platform": "win32", }, ) root.add_dependency(dependency_one) root.add_dependency(dependency_one_copy) cache = DependencyCache(provider) cache._search_for_cached.cache_clear() # ensure cache was never hit for both calls cache.search_for(dependency_one, 0) cache.search_for(dependency_one_copy, 0) assert not cache._search_for_cached.cache_info().hits # increase test coverage by searching for copies # (when searching for the exact same object, __eq__ is never called) packages_one = cache.search_for(deepcopy(dependency_one), 0) packages_one_copy = cache.search_for(deepcopy(dependency_one_copy), 0) assert cache._search_for_cached.cache_info().hits == 2 assert cache._search_for_cached.cache_info().currsize == 2 assert len(packages_one) == len(packages_one_copy) == 1 package_one = packages_one[0] package_one_copy = packages_one_copy[0] assert package_one.package.name == package_one_copy.package.name assert package_one.package.version.text == package_one_copy.package.version.text assert ( package_one.package.source_type == package_one_copy.package.source_type == "git" ) assert ( package_one.package.source_resolved_reference == package_one_copy.package.source_resolved_reference == MOCK_DEFAULT_GIT_REVISION ) assert ( package_one.package.source_subdirectory != package_one_copy.package.source_subdirectory ) assert package_one.package.source_subdirectory == "one" assert package_one_copy.package.source_subdirectory == "one-copy" assert package_one.dependency.marker.intersect( package_one_copy.dependency.marker ).is_empty() ================================================ FILE: tests/mixology/version_solver/test_python_constraint.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from poetry.factory import Factory from tests.mixology.helpers import add_to_repo from tests.mixology.helpers import check_solver_result if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage from poetry.repositories import Repository from tests.mixology.version_solver.conftest import Provider def test_dependency_does_not_match_root_python_constraint( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: provider.set_package_python_versions("^3.6") root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1.0.0", python="<3.5") error = """\ The current project's supported Python range (>=3.6,<4.0) is not compatible with some\ of the required packages Python requirement: - foo requires Python <3.5, so it will not be installable for Python >=3.6,<4.0 Because no versions of foo match !=1.0.0 and foo (1.0.0) requires Python <3.5, foo is forbidden. So, because myapp depends on foo (*), version solving failed. * Check your dependencies Python requirement: The Python requirement can be specified via the `python` or `markers` properties For foo, a possible solution would be to set the `python` property to "" https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies, https://python-poetry.org/docs/dependency-specification/#using-environment-markers """ check_solver_result(root, provider, error=error) ================================================ FILE: tests/mixology/version_solver/test_unsolvable.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.factory import Factory from poetry.puzzle.provider import IncompatibleConstraintsError from tests.mixology.helpers import add_to_repo from tests.mixology.helpers import check_solver_result if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage from poetry.repositories import Repository from tests.mixology.version_solver.conftest import Provider from tests.types import FixtureDirGetter def test_no_version_matching_constraint( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("foo", "^1.0")) add_to_repo(repo, "foo", "2.0.0") add_to_repo(repo, "foo", "2.1.3") check_solver_result( root, provider, error=( "Because myapp depends on foo (^1.0) " "which doesn't match any versions, version solving failed." ), ) def test_no_version_that_matches_combined_constraints( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"shared": ">=2.0.0 <3.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"shared": ">=2.9.0 <4.0.0"}) add_to_repo(repo, "shared", "2.5.0") add_to_repo(repo, "shared", "3.5.0") error = """\ Because foo (1.0.0) depends on shared (>=2.0.0 <3.0.0) and no versions of shared match >=2.9.0,<3.0.0,\ foo (1.0.0) requires shared (>=2.0.0,<2.9.0). And because bar (1.0.0) depends on shared (>=2.9.0 <4.0.0),\ bar (1.0.0) is incompatible with foo (1.0.0). So, because myapp depends on both foo (1.0.0) and bar (1.0.0), version solving failed.\ """ check_solver_result(root, provider, error=error) def test_disjoint_constraints( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"shared": "<=2.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"shared": ">3.0.0"}) add_to_repo(repo, "shared", "2.0.0") add_to_repo(repo, "shared", "4.0.0") error = """\ Because foo (1.0.0) depends on shared (<=2.0.0) and bar (1.0.0) depends on shared (>3.0.0),\ foo (1.0.0) is incompatible with bar (1.0.0). So, because myapp depends on both foo (1.0.0) and bar (1.0.0), version solving failed.\ """ check_solver_result(root, provider, error=error) check_solver_result(root, provider, error=error) def test_disjoint_root_constraints( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("foo", "2.0.0")) add_to_repo(repo, "foo", "1.0.0") add_to_repo(repo, "foo", "2.0.0") error = """\ Incompatible constraints in requirements of myapp (0.0.0): foo (==1.0.0) foo (==2.0.0)""" with pytest.raises(IncompatibleConstraintsError) as e: check_solver_result(root, provider, error=error) assert str(e.value) == error def test_disjoint_root_constraints_path_dependencies( root: ProjectPackage, provider: Provider, repo: Repository, fixture_dir: FixtureDirGetter, ) -> None: provider.set_package_python_versions("^3.7") project_dir = fixture_dir("with_conditional_path_deps") dependency1 = Factory.create_dependency("demo", {"path": project_dir / "demo_one"}) root.add_dependency(dependency1) dependency2 = Factory.create_dependency("demo", {"path": project_dir / "demo_two"}) root.add_dependency(dependency2) error = f"""\ Incompatible constraints in requirements of myapp (0.0.0): demo @ {project_dir.as_uri()}/demo_two (1.2.3) demo @ {project_dir.as_uri()}/demo_one (1.2.3)""" with pytest.raises(IncompatibleConstraintsError) as e: check_solver_result(root, provider, error=error) assert str(e.value) == error def test_no_valid_solution( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "1.0.0", deps={"b": "1.0.0"}) add_to_repo(repo, "a", "2.0.0", deps={"b": "2.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"a": "2.0.0"}) add_to_repo(repo, "b", "2.0.0", deps={"a": "1.0.0"}) error = """\ Because no versions of b match <1.0.0 || >1.0.0,<2.0.0 || >2.0.0 and b (1.0.0) depends on a (2.0.0), b (!=2.0.0) requires a (2.0.0). And because a (2.0.0) depends on b (2.0.0), b is forbidden. Because b (2.0.0) depends on a (1.0.0) which depends on b (1.0.0), b is forbidden. Thus, b is forbidden. So, because myapp depends on b (*), version solving failed.""" check_solver_result(root, provider, error=error, tries=2) def test_package_with_the_same_name_gives_clear_error_message( root: ProjectPackage, provider: Provider, repo: Repository ) -> None: pkg_name = "a" root.add_dependency(Factory.create_dependency(pkg_name, "*")) add_to_repo(repo, pkg_name, "1.0.0", deps={pkg_name: "1.0.0"}) error = f"Package '{pkg_name}' is listed as a dependency of itself." check_solver_result(root, provider, error=error) ================================================ FILE: tests/mixology/version_solver/test_with_lock.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from cleo.io.null_io import NullIO from packaging.utils import canonicalize_name from poetry.core.packages.package import Package from poetry.factory import Factory from tests.helpers import get_package from tests.mixology.helpers import add_to_repo from tests.mixology.helpers import check_solver_result from tests.mixology.version_solver.conftest import Provider if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage from poetry.repositories import Repository from poetry.repositories import RepositoryPool def test_with_compatible_locked_dependencies( root: ProjectPackage, repo: Repository, pool: RepositoryPool ) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "foo", "1.0.1", deps={"bar": "1.0.1"}) add_to_repo(repo, "foo", "1.0.2", deps={"bar": "1.0.2"}) add_to_repo(repo, "bar", "1.0.0") add_to_repo(repo, "bar", "1.0.1") add_to_repo(repo, "bar", "1.0.2") locked = [get_package("foo", "1.0.1"), get_package("bar", "1.0.1")] provider = Provider(root, pool, NullIO(), locked=locked) check_solver_result( root, provider, result={"foo": "1.0.1", "bar": "1.0.1"}, ) def test_with_incompatible_locked_dependencies( root: ProjectPackage, repo: Repository, pool: RepositoryPool ) -> None: root.add_dependency(Factory.create_dependency("foo", ">1.0.1")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "foo", "1.0.1", deps={"bar": "1.0.1"}) add_to_repo(repo, "foo", "1.0.2", deps={"bar": "1.0.2"}) add_to_repo(repo, "bar", "1.0.0") add_to_repo(repo, "bar", "1.0.1") add_to_repo(repo, "bar", "1.0.2") locked = [get_package("foo", "1.0.1"), get_package("bar", "1.0.1")] provider = Provider(root, pool, NullIO(), locked=locked) check_solver_result( root, provider, result={"foo": "1.0.2", "bar": "1.0.2"}, ) def test_with_unrelated_locked_dependencies( root: ProjectPackage, repo: Repository, pool: RepositoryPool ) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "foo", "1.0.1", deps={"bar": "1.0.1"}) add_to_repo(repo, "foo", "1.0.2", deps={"bar": "1.0.2"}) add_to_repo(repo, "bar", "1.0.0") add_to_repo(repo, "bar", "1.0.1") add_to_repo(repo, "bar", "1.0.2") add_to_repo(repo, "baz", "1.0.0") locked = [get_package("baz", "1.0.1")] provider = Provider(root, pool, NullIO(), locked=locked) check_solver_result( root, provider, result={"foo": "1.0.2", "bar": "1.0.2"}, ) def test_unlocks_dependencies_if_necessary_to_ensure_that_a_new_dependency_is_satisfied( root: ProjectPackage, repo: Repository, pool: RepositoryPool ) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) root.add_dependency(Factory.create_dependency("newdep", "2.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "<2.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"baz": "<2.0.0"}) add_to_repo(repo, "baz", "1.0.0", deps={"qux": "<2.0.0"}) add_to_repo(repo, "qux", "1.0.0") add_to_repo(repo, "foo", "2.0.0", deps={"bar": "<3.0.0"}) add_to_repo(repo, "bar", "2.0.0", deps={"baz": "<3.0.0"}) add_to_repo(repo, "baz", "2.0.0", deps={"qux": "<3.0.0"}) add_to_repo(repo, "qux", "2.0.0") add_to_repo(repo, "newdep", "2.0.0", deps={"baz": ">=1.5.0"}) locked = [ get_package("foo", "2.0.0"), get_package("bar", "1.0.0"), get_package("baz", "1.0.0"), get_package("qux", "1.0.0"), ] provider = Provider(root, pool, NullIO(), locked=locked) check_solver_result( root, provider, result={ "foo": "2.0.0", "bar": "2.0.0", "baz": "2.0.0", "qux": "1.0.0", "newdep": "2.0.0", }, ) def test_with_compatible_locked_dependencies_use_latest( root: ProjectPackage, repo: Repository, pool: RepositoryPool ) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) root.add_dependency(Factory.create_dependency("baz", "*")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "foo", "1.0.1", deps={"bar": "1.0.1"}) add_to_repo(repo, "foo", "1.0.2", deps={"bar": "1.0.2"}) add_to_repo(repo, "bar", "1.0.0") add_to_repo(repo, "bar", "1.0.1") add_to_repo(repo, "bar", "1.0.2") add_to_repo(repo, "baz", "1.0.0") add_to_repo(repo, "baz", "1.0.1") locked = [ get_package("foo", "1.0.1"), get_package("bar", "1.0.1"), get_package("baz", "1.0.0"), ] provider = Provider(root, pool, NullIO(), locked=locked) check_solver_result( root, provider, result={"foo": "1.0.2", "bar": "1.0.2", "baz": "1.0.0"}, use_latest=[canonicalize_name("foo")], ) def test_with_compatible_locked_dependencies_with_extras( root: ProjectPackage, repo: Repository, pool: RepositoryPool ) -> None: root.add_dependency(Factory.create_dependency("foo", "^1.0")) package_foo_0 = get_package("foo", "1.0.0") package_foo_1 = get_package("foo", "1.0.1") bar_extra_dep = Factory.create_dependency( "bar", {"version": "^1.0", "extras": "extra"} ) for package_foo in (package_foo_0, package_foo_1): package_foo.add_dependency(bar_extra_dep) repo.add_package(package_foo) bar_deps = {"baz": {"version": "^1.0", "extras": ["extra"]}} add_to_repo(repo, "bar", "1.0.0", bar_deps) add_to_repo(repo, "bar", "1.0.1", bar_deps) add_to_repo(repo, "baz", "1.0.0") add_to_repo(repo, "baz", "1.0.1") locked = [ get_package("foo", "1.0.0"), get_package("bar", "1.0.0"), get_package("baz", "1.0.0"), ] provider = Provider(root, pool, NullIO(), locked=locked) check_solver_result( root, provider, result={"foo": "1.0.0", "bar": "1.0.0", "baz": "1.0.0"}, ) def test_with_yanked_package_in_lock( root: ProjectPackage, repo: Repository, pool: RepositoryPool ) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1") add_to_repo(repo, "foo", "2", yanked=True) # yanked version is kept in lock file locked_foo = get_package("foo", "2") assert not locked_foo.yanked provider = Provider(root, pool, NullIO(), locked=[locked_foo]) result = check_solver_result( root, provider, result={"foo": "2"}, ) assert result is not None foo = result.packages[0] assert foo.yanked # without considering the lock file, the other version is chosen provider = Provider(root, pool, NullIO()) check_solver_result( root, provider, result={"foo": "1"}, ) def test_no_update_is_respected_for_legacy_repository( root: ProjectPackage, repo: Repository, pool: RepositoryPool ) -> None: root.add_dependency(Factory.create_dependency("foo", "^1.0")) foo_100 = Package( "foo", "1.0.0", source_type="legacy", source_url="http://example.com" ) foo_101 = Package( "foo", "1.0.1", source_type="legacy", source_url="http://example.com" ) repo.add_package(foo_100) repo.add_package(foo_101) provider = Provider(root, pool, NullIO(), locked=[foo_100]) check_solver_result( root, provider, result={"foo": "1.0.0"}, ) ================================================ FILE: tests/packages/__init__.py ================================================ ================================================ FILE: tests/packages/test_direct_origin.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from unittest.mock import MagicMock from poetry.core.packages.utils.link import Link from poetry.packages.direct_origin import DirectOrigin from poetry.utils.cache import ArtifactCache if TYPE_CHECKING: from pathlib import Path from pytest_mock import MockerFixture from tests.types import FixtureDirGetter def test_direct_origin_get_package_from_file(fixture_dir: FixtureDirGetter) -> None: wheel_path = fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl" package = DirectOrigin.get_package_from_file(wheel_path) assert package.name == "demo" assert package.files == [ { "file": "demo-0.1.2-py2.py3-none-any.whl", "hash": "sha256:55dde4e6828081de7a1e429f33180459c333d9da593db62a3d75a8f5e505dde1", "size": 1552, } ] def test_direct_origin_caches_url_dependency(tmp_path: Path) -> None: artifact_cache = ArtifactCache(cache_dir=tmp_path) direct_origin = DirectOrigin(artifact_cache) url = "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" package = direct_origin.get_package_from_url(url) assert package.name == "demo" assert package.files == [ { "file": "demo-0.1.0-py2.py3-none-any.whl", "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", "size": 1116, } ] assert artifact_cache.get_cached_archive_for_link(Link(url), strict=True) def test_direct_origin_does_not_download_url_dependency_when_cached( fixture_dir: FixtureDirGetter, mocker: MockerFixture ) -> None: artifact_cache = MagicMock() artifact_cache.get_cached_archive_for_link = MagicMock( return_value=fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl" ) direct_origin = DirectOrigin(artifact_cache) url = "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" download_file = mocker.patch( "poetry.packages.direct_origin.DirectOrigin._download_file", side_effect=Exception("download_file should not be called"), ) package = direct_origin.get_package_from_url(url) assert package.name == "demo" assert package.files == [ { "file": "demo-0.1.2-py2.py3-none-any.whl", "hash": "sha256:55dde4e6828081de7a1e429f33180459c333d9da593db62a3d75a8f5e505dde1", "size": 1552, } ] artifact_cache.get_cached_archive_for_link.assert_called_once_with( Link(url), strict=True, download_func=download_file ) ================================================ FILE: tests/packages/test_locker.py ================================================ from __future__ import annotations import json import logging import os import re import sys import tempfile import uuid from hashlib import sha256 from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Literal import pytest from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import parse_marker from poetry.__version__ import __version__ from poetry.factory import Factory from poetry.packages.locker import GENERATED_COMMENT from poetry.packages.locker import Locker from poetry.packages.transitive_package_info import TransitivePackageInfo from tests.helpers import get_dependency from tests.helpers import get_package if TYPE_CHECKING: from pytest import LogCaptureFixture from pytest_mock import MockerFixture DEV_GROUP = canonicalize_name("dev") @pytest.fixture def locker() -> Locker: with tempfile.NamedTemporaryFile() as f: f.close() locker = Locker(Path(f.name), {}) return locker @pytest.fixture def root() -> ProjectPackage: return ProjectPackage("root", "1.2.3") @pytest.fixture def transitive_info() -> TransitivePackageInfo: return TransitivePackageInfo(0, {MAIN_GROUP}, {}) @pytest.mark.parametrize("is_locked", [True, False]) def test_is_locked(locker: Locker, root: ProjectPackage, is_locked: bool) -> None: if is_locked: locker.set_lock_data(root, {}) assert locker.is_locked() is is_locked @pytest.mark.parametrize("is_fresh", [True, False]) def test_is_fresh( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo, is_fresh: bool, ) -> None: locker.set_lock_data(root, {}) if not is_fresh: locker.set_pyproject_data( {"tool": {"poetry": {"dependencies": {"tomli": "*"}}}} ) assert locker.is_fresh() is is_fresh @pytest.mark.parametrize( ("kind", "version", "expected"), [ ("valid", "2.3.0", True), ("legacy", "2.3.0", False), ("outdated", "2.3.0", False), ("valid", "2.2.1", True), ("legacy", "2.2.1", True), ("outdated", "2.3.0", False), ], ) def test_is_fresh_dependency_groups( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo, kind: Literal["valid", "legacy", "outdated"], version: str, expected: bool, ) -> None: locker.set_lock_data(root, {}) locker.set_pyproject_data({"dependency-groups": {"foo": []}}) if kind == "valid": locked_hash = locker._get_content_hash() elif kind == "legacy": locked_hash = locker._get_content_hash(with_dependency_groups=False) assert locked_hash != locker._get_content_hash() else: locked_hash = "123456" lock_content = locker.lock.read_text(encoding="utf-8") lock_content = re.sub(r"Poetry [^ ]+", f"Poetry {version}", lock_content) lock_content = re.sub( r'content-hash = "[^"]+"', f'content-hash = "{locked_hash}"', lock_content ) locker.lock.write_text(lock_content, encoding="utf-8") assert locker.is_fresh() is expected @pytest.mark.parametrize("lock_version", [None, "2.0", "2.1"]) def test_is_locked_group_and_markers( locker: Locker, root: ProjectPackage, lock_version: str | None ) -> None: if lock_version: locker.set_lock_data(root, {}) with locker.lock.open("r", encoding="utf-8") as f: content = f.read() content = content.replace(locker._VERSION, lock_version) with locker.lock.open("w", encoding="utf-8") as f: f.write(content) assert locker.is_locked_groups_and_markers() is (lock_version == "2.1") def test_lock_file_data_is_ordered( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0")) package_a.files = [{"file": "foo", "hash": "456"}, {"file": "bar", "hash": "123"}] package_a2 = get_package("A", "2.0.0") package_a2.files = [{"file": "baz", "hash": "345"}] package_git = Package( "git-package", "1.2.3", source_type="git", source_url="https://github.com/python-poetry/poetry.git", source_reference="develop", source_resolved_reference="123456", ) package_git_with_subdirectory = Package( "git-package-subdir", "1.2.3", source_type="git", source_url="https://github.com/python-poetry/poetry.git", source_reference="develop", source_resolved_reference="123456", source_subdirectory="subdir", ) package_url_linux = Package( "url-package", "1.0", source_type="url", source_url="https://example.org/url-package-1.0-cp39-manylinux_2_17_x86_64.whl", ) package_url_win32 = Package( "url-package", "1.0", source_type="url", source_url="https://example.org/url-package-1.0-cp39-win_amd64.whl", ) packages = { package_a2: transitive_info, package_a: transitive_info, get_package("B", "1.2"): transitive_info, package_git: transitive_info, package_git_with_subdirectory: transitive_info, package_url_win32: transitive_info, package_url_linux: transitive_info, } locker.set_lock_data(root, packages) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [ {{file = "bar", hash = "123"}}, {{file = "foo", hash = "456"}}, ] [package.dependencies] B = "^1.0" [[package]] name = "A" version = "2.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [ {{file = "baz", hash = "345"}}, ] [[package]] name = "B" version = "1.2" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "git-package" version = "1.2.3" description = "" optional = false python-versions = "*" groups = ["main"] files = [] develop = false [package.source] type = "git" url = "https://github.com/python-poetry/poetry.git" reference = "develop" resolved_reference = "123456" [[package]] name = "git-package-subdir" version = "1.2.3" description = "" optional = false python-versions = "*" groups = ["main"] files = [] develop = false [package.source] type = "git" url = "https://github.com/python-poetry/poetry.git" reference = "develop" resolved_reference = "123456" subdirectory = "subdir" [[package]] name = "url-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.source] type = "url" url = "https://example.org/url-package-1.0-cp39-manylinux_2_17_x86_64.whl" [[package]] name = "url-package" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.source] type = "url" url = "https://example.org/url-package-1.0-cp39-win_amd64.whl" [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_locker_properly_loads_extras(locker: Locker) -> None: content = f"""\ # {GENERATED_COMMENT} [[package]] name = "cachecontrol" version = "0.12.5" description = "httplib2 caching for requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] files = [] [package.dependencies] msgpack = "*" requests = "*" [package.dependencies.lockfile] optional = true version = ">=0.9" [package.extras] filecache = ["lockfile (>=0.9)"] redis = ["redis (>=2.10.5)"] [metadata] lock-version = "2.1" python-versions = "~2.7 || ^3.4" content-hash = "c3d07fca33fba542ef2b2a4d75bf5b48d892d21a830e2ad9c952ba5123a52f77" """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) packages = locker.locked_repository().packages assert len(packages) == 1 package = packages[0] assert len(package.requires) == 3 assert len(package.extras) == 2 lockfile_dep = package.extras[canonicalize_name("filecache")][0] assert lockfile_dep.name == "lockfile" def test_locker_properly_loads_nested_extras(locker: Locker) -> None: content = f"""\ # {GENERATED_COMMENT} [[package]] name = "a" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] b = {{version = "^1.0", optional = true, extras = "c"}} [package.extras] b = ["b[c] (>=1.0,<2.0)"] [[package]] name = "b" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] c = {{version = "^1.0", optional = true}} [package.extras] c = ["c (>=1.0,<2.0)"] [[package]] name = "c" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) repository = locker.locked_repository() assert len(repository.packages) == 3 packages = repository.find_packages(get_dependency("a", "1.0")) assert len(packages) == 1 package = packages[0] assert len(package.requires) == 1 assert len(package.extras) == 1 dependency_b = package.extras[canonicalize_name("b")][0] assert dependency_b.name == "b" assert dependency_b.extras == frozenset({"c"}) packages = repository.find_packages(dependency_b) assert len(packages) == 1 package = packages[0] assert len(package.requires) == 1 assert len(package.extras) == 1 dependency_c = package.extras[canonicalize_name("c")][0] assert dependency_c.name == "c" assert dependency_c.extras == frozenset() packages = repository.find_packages(dependency_c) assert len(packages) == 1 def test_locker_properly_loads_extras_legacy(locker: Locker) -> None: content = f"""\ # {GENERATED_COMMENT} [[package]] name = "a" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] b = {{version = "^1.0", optional = true}} [package.extras] b = ["b (^1.0)"] [[package]] name = "b" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] python-versions = "*" lock-version = "2.1" content-hash = "123456789" """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) repository = locker.locked_repository() assert len(repository.packages) == 2 packages = repository.find_packages(get_dependency("a", "1.0")) assert len(packages) == 1 package = packages[0] assert len(package.requires) == 1 assert len(package.extras) == 1 dependency_b = package.extras[canonicalize_name("b")][0] assert dependency_b.name == "b" def test_locker_properly_loads_subdir(locker: Locker) -> None: content = """\ [[package]] name = "git-package-subdir" version = "1.2.3" description = "" optional = false python-versions = "*" groups = ["main"] develop = false files = [] [package.source] type = "git" url = "https://github.com/python-poetry/poetry.git" reference = "develop" resolved_reference = "123456" subdirectory = "subdir" [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) repository = locker.locked_repository() assert len(repository.packages) == 1 packages = repository.find_packages(get_dependency("git-package-subdir", "1.2.3")) assert len(packages) == 1 package = packages[0] assert package.source_subdirectory == "subdir" @pytest.mark.parametrize( ("groups", "marker", "expected"), [ # only main - without marker (["main"], None, {"main": "*"}), # only main - with marker ( ["main"], repr('python_version == "3.9"'), {"main": 'python_version == "3.9"'}, ), # two groups - common marker ( ["main", "dev"], repr('python_version == "3.9"'), {"main": 'python_version == "3.9"', "dev": 'python_version == "3.9"'}, ), # two groups - separate marker ( ["main", "dev"], ( '{"main" = \'python_version == "3.9"\',' ' "dev" = \'sys_platform == "linux"\'}' ), {"main": 'python_version == "3.9"', "dev": 'sys_platform == "linux"'}, ), # two groups - one without marker ( ["main", "dev"], '{"main" = \'python_version == "3.9"\'}', {"main": 'python_version == "3.9"', "dev": "*"}, ), ( # unnormalized group - common marker ["main", "DEV"], repr('python_version == "3.9"'), {"main": 'python_version == "3.9"', "dev": 'python_version == "3.9"'}, ), ( # unnormalized group - separate marker ["main", "DEV"], ( '{"main" = \'python_version == "3.9"\',' ' "DEV" = \'sys_platform == "linux"\'}' ), {"main": 'python_version == "3.9"', "dev": 'sys_platform == "linux"'}, ), ], ) def test_locker_properly_loads_groups_and_markers( locker: Locker, groups: list[str], marker: str, expected: dict[str, str] ) -> None: content = rf""" [[package]] name = "a" version = "1.0" optional = false python-versions = "*" groups = {groups} {"markers = " + marker if marker else ""} files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) packages = locker.locked_packages() a = get_package("a", "1.0") assert len(packages) == 1 assert packages[a].groups == {canonicalize_name(g) for g in groups} assert packages[a].markers == {g: parse_marker(m) for g, m in expected.items()} def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: """ For multiple constraints dependencies, there is only one common entry in metadata.files. However, we must not assign all the files to each of the packages because this can result in duplicated and outdated entries when running `poetry lock` and hash check failures when running `poetry install`. """ content = """\ [[package]] name = "demo" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] develop = false [[package]] name = "demo" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] develop = false [package.source] type = "git" url = "https://github.com/demo/demo.git" reference = "main" resolved_reference = "123456" [[package]] name = "demo" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] develop = false [package.source] type = "directory" url = "./folder" [[package]] name = "demo" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] develop = false [package.source] type = "file" url = "./demo-1.0-cp39-win_amd64.whl" [[package]] name = "demo" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] develop = false [package.source] type = "url" url = "https://example.com/demo-1.0-cp38-win_amd64.whl" [metadata] lock-version = "1.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" [metadata.files] # metadata.files are only tracked for non-direct origin and file dependencies demo = [ {file = "demo-1.0-cp39-win_amd64.whl", hash = "sha256"}, {file = "demo-1.0.tar.gz", hash = "sha256"}, {file = "demo-1.0-py3-none-any.whl", hash = "sha256"}, ] """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) repository = locker.locked_repository() assert len(repository.packages) == 5 assert {package.source_type for package in repository.packages} == { None, "git", "directory", "file", "url", } for package in repository.packages: if package.source_type is None: # non-direct origin package contains all files # with the current lockfile format we have no chance to determine # which files are correct, so we keep all for hash check # correct files are set later in Provider.complete_package() assert package.files == [ {"file": "demo-1.0-cp39-win_amd64.whl", "hash": "sha256"}, {"file": "demo-1.0.tar.gz", "hash": "sha256"}, {"file": "demo-1.0-py3-none-any.whl", "hash": "sha256"}, ] elif package.source_type == "file": assert package.files == [ {"file": "demo-1.0-cp39-win_amd64.whl", "hash": "sha256"} ] else: package.files = [] def test_locker_dumps_packages_with_null_description( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: package_a = get_package("A", "1.0.0") package_a.description = None # type: ignore[assignment] locker.set_lock_data(root, {package_a: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_locker_does_not_dump_file_urls( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: package_a = get_package("A", "1.0") package_a.files = [ { "file": "a-1.0.whl", "hash": "sha256:abcdef1234567890", "url": "https://example.org/a-1.0.whl", }, ] locker.set_lock_data(root, {package_a: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [ {{file = "a-1.0.whl", hash = "sha256:abcdef1234567890"}}, ] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_lock_file_should_not_have_mixed_types( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0.0")) package_a.add_dependency( Factory.create_dependency("B", {"version": ">=1.0.0", "optional": True}) ) package_a.requires[-1].activate() package_a.extras = {canonicalize_name("foo"): [get_dependency("B", ">=1.0.0")]} locker.set_lock_data(root, {package_a: transitive_info}) expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] B = [ {{version = "^1.0.0"}}, {{version = ">=1.0.0", optional = true}}, ] [package.extras] foo = ["B (>=1.0.0)"] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with locker.lock.open(encoding="utf-8") as f: content = f.read() assert content == expected def test_reading_lock_file_should_raise_an_error_on_invalid_data( locker: Locker, ) -> None: content = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.extras] foo = ["bar"] [package.extras] foo = ["bar"] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with locker.lock.open("w", encoding="utf-8") as f: f.write(content) with pytest.raises(RuntimeError) as e: _ = locker.lock_data assert "Unable to read the lock file" in str(e.value) def test_reading_lock_file_should_raise_an_error_on_missing_metadata( locker: Locker, ) -> None: content = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.source] type = "legacy" url = "https://foo.bar" reference = "legacy" """ with locker.lock.open("w", encoding="utf-8") as f: f.write(content) with pytest.raises(RuntimeError) as e: _ = locker.lock_data assert ( "The lock file does not have a metadata entry.\nRegenerate the lock file with" " the `poetry lock` command." in str(e.value) ) def test_locking_legacy_repository_package_should_include_source_section( root: ProjectPackage, locker: Locker, transitive_info: TransitivePackageInfo ) -> None: package_a = Package( "A", "1.0.0", source_type="legacy", source_url="https://foo.bar", source_reference="legacy", ) packages = {package_a: transitive_info} locker.set_lock_data(root, packages) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.source] type = "legacy" url = "https://foo.bar" reference = "legacy" [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_locker_should_emit_warnings_if_lock_version_is_newer_but_allowed( locker: Locker, caplog: LogCaptureFixture ) -> None: version = ".".join(Version.parse(Locker._VERSION).next_minor().text.split(".")[:2]) content = f"""\ [metadata] lock-version = "{version}" python-versions = "~2.7 || ^3.4" content-hash = "c3d07fca33fba542ef2b2a4d75bf5b48d892d21a830e2ad9c952ba5123a52f77" """ caplog.set_level(logging.WARNING, logger="poetry.packages.locker") with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) _ = locker.lock_data assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == "WARNING" expected = """\ The lock file might not be compatible with the current version of Poetry. Upgrade Poetry to ensure the lock file is read properly or, alternatively, \ regenerate the lock file with the `poetry lock` command.\ """ assert record.message == expected def test_locker_should_raise_an_error_if_lock_version_is_newer_and_not_allowed( locker: Locker, caplog: LogCaptureFixture ) -> None: content = f"""\ # {GENERATED_COMMENT} [metadata] lock-version = "3.0" python-versions = "~2.7 || ^3.4" content-hash = "c3d07fca33fba542ef2b2a4d75bf5b48d892d21a830e2ad9c952ba5123a52f77" """ caplog.set_level(logging.WARNING, logger="poetry.packages.locker") with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) with pytest.raises(RuntimeError, match=r"^The lock file is not compatible"): _ = locker.lock_data def test_locker_should_raise_an_error_if_no_lock_version( locker: Locker, caplog: LogCaptureFixture ) -> None: """Lock file prior Poetry 1.1 have no lock file version.""" content = """\ [metadata] python-versions = "~2.7 || ^3.4" content-hash = "c3d07fca33fba542ef2b2a4d75bf5b48d892d21a830e2ad9c952ba5123a52f77" """ caplog.set_level(logging.WARNING, logger="poetry.packages.locker") with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) with pytest.raises(RuntimeError, match=r"^The lock file is not compatible"): _ = locker.lock_data def test_root_extras_dependencies_are_ordered( locker: Locker, root: ProjectPackage, fixture_base: Path ) -> None: Factory.create_dependency("B", "1.0.0", root_dir=fixture_base) Factory.create_dependency("C", "1.0.0", root_dir=fixture_base) package_first = Factory.create_dependency("first", "1.0.0", root_dir=fixture_base) package_second = Factory.create_dependency("second", "1.0.0", root_dir=fixture_base) package_third = Factory.create_dependency("third", "1.0.0", root_dir=fixture_base) root.extras = { canonicalize_name("C"): [package_third, package_second, package_first], canonicalize_name("B"): [package_first, package_second, package_third], } locker.set_lock_data(root, {}) expected = f"""\ # {GENERATED_COMMENT} package = [] [extras] b = ["first", "second", "third"] c = ["first", "second", "third"] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with locker.lock.open(encoding="utf-8") as f: content = f.read() assert content == expected def test_extras_dependencies_are_ordered( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency( Factory.create_dependency( "B", {"version": "^1.0.0", "optional": True, "extras": ["c", "a", "b"]} ) ) package_a.requires[-1].activate() locker.set_lock_data(root, {package_a: transitive_info}) expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] B = {{version = "^1.0.0", extras = ["a", "b", "c"], optional = true}} [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with locker.lock.open(encoding="utf-8") as f: content = f.read() assert content == expected def test_locker_should_neither_emit_warnings_nor_raise_error_for_lower_compatible_versions( locker: Locker, caplog: LogCaptureFixture ) -> None: older_version = "1.1" content = f"""\ [metadata] lock-version = "{older_version}" python-versions = "~2.7 || ^3.4" content-hash = "c3d07fca33fba542ef2b2a4d75bf5b48d892d21a830e2ad9c952ba5123a52f77" [metadata.files] """ caplog.set_level(logging.WARNING, logger="poetry.packages.locker") with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) _ = locker.lock_data assert len(caplog.records) == 0 def test_locker_dumps_groups_and_markers( locker: Locker, root: ProjectPackage, fixture_base: Path, transitive_info: TransitivePackageInfo, ) -> None: packages = { get_package("A", "1.0"): TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: AnyMarker()} ), get_package("B", "1.0"): TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker('sys_platform == "win32"')} ), get_package("C", "1.0"): TransitivePackageInfo( 0, {MAIN_GROUP, DEV_GROUP}, {MAIN_GROUP: AnyMarker(), DEV_GROUP: AnyMarker()}, ), get_package("D", "1.0"): TransitivePackageInfo( 0, {MAIN_GROUP, DEV_GROUP}, { MAIN_GROUP: parse_marker('sys_platform == "win32"'), DEV_GROUP: parse_marker('sys_platform == "win32"'), }, ), get_package("E", "1.0"): TransitivePackageInfo( 0, {MAIN_GROUP, DEV_GROUP}, { MAIN_GROUP: parse_marker('sys_platform == "win32"'), DEV_GROUP: parse_marker('sys_platform == "linux"'), }, ), get_package("F", "1.0"): TransitivePackageInfo( 0, {MAIN_GROUP, DEV_GROUP}, { MAIN_GROUP: parse_marker('sys_platform == "win32"'), DEV_GROUP: AnyMarker(), }, ), } locker.set_lock_data(root, packages) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [[package]] name = "B" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main"] markers = "sys_platform == \\"win32\\"" files = [] [[package]] name = "C" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main", "dev"] files = [] [[package]] name = "D" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main", "dev"] markers = "sys_platform == \\"win32\\"" files = [] [[package]] name = "E" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main", "dev"] files = [] markers = {{main = "sys_platform == \\"win32\\"", dev = "sys_platform == \\"linux\\""}} [[package]] name = "F" version = "1.0" description = "" optional = false python-versions = "*" groups = ["main", "dev"] files = [] markers = {{main = "sys_platform == \\"win32\\""}} [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_locker_dumps_dependency_information_correctly( locker: Locker, root: ProjectPackage, fixture_base: Path, transitive_info: TransitivePackageInfo, ) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency( Factory.create_dependency( "B", {"path": "project_with_extras", "develop": True}, root_dir=fixture_base ) ) package_a.add_dependency( Factory.create_dependency( "C", {"path": "directory/project_with_transitive_directory_dependencies"}, root_dir=fixture_base, ) ) package_a.add_dependency( Factory.create_dependency( "D", {"path": "distributions/demo-0.1.0.tar.gz"}, root_dir=fixture_base ) ) package_a.add_dependency( Factory.create_dependency( "E", {"url": "https://files.pythonhosted.org/poetry-1.2.0.tar.gz"} ) ) package_a.add_dependency( Factory.create_dependency( "F", {"git": "https://github.com/python-poetry/poetry.git", "branch": "foo"} ) ) package_a.add_dependency( Factory.create_dependency( "G", { "git": "https://github.com/python-poetry/poetry.git", "subdirectory": "bar", }, ) ) package_a.add_dependency( Factory.create_dependency( "H", {"git": "https://github.com/python-poetry/poetry.git", "tag": "baz"} ) ) package_a.add_dependency( Factory.create_dependency( "I", {"git": "https://github.com/python-poetry/poetry.git", "rev": "spam"} ) ) packages = {package_a: transitive_info} locker.set_lock_data(root, packages) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] B = {{path = "project_with_extras", develop = true}} C = {{path = "directory/project_with_transitive_directory_dependencies"}} D = {{path = "distributions/demo-0.1.0.tar.gz"}} E = {{url = "https://files.pythonhosted.org/poetry-1.2.0.tar.gz"}} F = {{git = "https://github.com/python-poetry/poetry.git", branch = "foo"}} G = {{git = "https://github.com/python-poetry/poetry.git", subdirectory = "bar"}} H = {{git = "https://github.com/python-poetry/poetry.git", tag = "baz"}} I = {{git = "https://github.com/python-poetry/poetry.git", rev = "spam"}} [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_locker_dumps_subdir( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: package_git_with_subdirectory = Package( "git-package-subdir", "1.2.3", source_type="git", source_url="https://github.com/python-poetry/poetry.git", source_reference="develop", source_resolved_reference="123456", source_subdirectory="subdir", ) locker.set_lock_data(root, {package_git_with_subdirectory: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "git-package-subdir" version = "1.2.3" description = "" optional = false python-versions = "*" groups = ["main"] files = [] develop = false [package.source] type = "git" url = "https://github.com/python-poetry/poetry.git" reference = "develop" resolved_reference = "123456" subdirectory = "subdir" [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_locker_dumps_dependency_extras_in_correct_order( locker: Locker, root: ProjectPackage, fixture_base: Path, transitive_info: TransitivePackageInfo, ) -> None: package_a = get_package("A", "1.0.0") Factory.create_dependency("B", "1.0.0", root_dir=fixture_base) Factory.create_dependency("C", "1.0.0", root_dir=fixture_base) package_first = Factory.create_dependency("first", "1.0.0", root_dir=fixture_base) package_second = Factory.create_dependency("second", "1.0.0", root_dir=fixture_base) package_third = Factory.create_dependency("third", "1.0.0", root_dir=fixture_base) package_a.extras = { canonicalize_name("C"): [package_third, package_second, package_first], canonicalize_name("B"): [package_first, package_second, package_third], } locker.set_lock_data(root, {package_a: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.extras] b = ["first (==1.0.0)", "second (==1.0.0)", "third (==1.0.0)"] c = ["first (==1.0.0)", "second (==1.0.0)", "third (==1.0.0)"] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_locker_dumps_extras_with_constraints( locker: Locker, root: ProjectPackage, fixture_base: Path, transitive_info: TransitivePackageInfo, ) -> None: package_a = get_package("A", "1.0.0") package_httpx_new = Factory.create_dependency( "httpx", {"version": "2.0.0", "python": ">=3.7", "extras": ["brotli"]}, root_dir=fixture_base, ) package_httpx_old = Factory.create_dependency( "httpx", {"version": "1.0.0", "python": "<3.7"}, root_dir=fixture_base ) package_a.extras = { canonicalize_name("http"): [package_httpx_new, package_httpx_old], } locker.set_lock_data(root, {package_a: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.extras] http = ["httpx (==1.0.0) ; python_version < \\"3.7\\"", "httpx[brotli] (==2.0.0) ; python_version >= \\"3.7\\""] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_locker_properly_loads_extras_with_constraints(locker: Locker) -> None: content = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.extras] http = ["httpx (==1.0.0) ; python_version < \\"3.7\\"", "httpx[brotli] (==2.0.0) ; python_version >= \\"3.7\\""] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) packages = locker.locked_repository().packages assert len(packages) == 1 package = packages[0] assert len(package.extras) == 1 httpx_deps = package.extras[canonicalize_name("http")] assert len(httpx_deps) == 2 assert httpx_deps[0].constraint == Version.parse("1.0.0") assert str(httpx_deps[0].python_constraint) == "<3.7" assert httpx_deps[1].constraint == Version.parse("2.0.0") assert str(httpx_deps[1].python_constraint) == ">=3.7" assert httpx_deps[1].extras == {"brotli"} def test_locked_repository_uses_root_dir_of_package( locker: Locker, mocker: MockerFixture ) -> None: content = f"""\ # {GENERATED_COMMENT} [[package]] name = "lib-a" version = "0.1.0" description = "" optional = false python-versions = "^2.7.9" groups = ["main"] develop = true file = [] [package.dependencies] lib-b = {{path = "../libB", develop = true}} [package.source] type = "directory" url = "lib/libA" [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(content) create_dependency_patch = mocker.patch( "poetry.factory.Factory.create_dependency", autospec=True ) locker.locked_repository() create_dependency_patch.assert_called_once_with( "lib-b", {"develop": True, "path": "../libB"}, root_dir=mocker.ANY ) call_kwargs = create_dependency_patch.call_args[1] root_dir = call_kwargs["root_dir"] assert root_dir.match("*/lib/libA") # relative_to raises an exception if not relative - is_relative_to comes in py3.9 assert root_dir.relative_to(locker.lock.parent.resolve()) is not None @pytest.mark.parametrize( ("local_config", "legacy"), [ ({}, True), ({"dependencies": [uuid.uuid4().hex]}, True), ({"dependencies": [uuid.uuid4().hex], "source": [uuid.uuid4().hex]}, True), ({"dependencies": [uuid.uuid4().hex], "extras": [uuid.uuid4().hex]}, True), ( { "dependencies": [uuid.uuid4().hex], "dev-dependencies": [uuid.uuid4().hex], }, True, ), ( { "dependencies": [uuid.uuid4().hex], "dev-dependencies": None, }, True, ), ({"dependencies": [uuid.uuid4().hex], "group": [uuid.uuid4().hex]}, False), ], ) def test_content_hash_with_legacy_is_compatible( local_config: dict[str, list[str]], legacy: bool, locker: Locker ) -> None: """Legacy generation if there is no group.""" def _get_legacy_content_hash() -> str: relevant_content = {} for key in locker._legacy_keys: relevant_content[key] = local_config.get(key) content_hash = sha256( json.dumps(relevant_content, sort_keys=True).encode() ).hexdigest() return content_hash locker = locker.__class__( lock=locker.lock, pyproject_data={"tool": {"poetry": local_config}}, ) old_content_hash = _get_legacy_content_hash() content_hash = locker._get_content_hash() if legacy: assert content_hash == old_content_hash else: assert content_hash != old_content_hash @pytest.mark.parametrize( ("project", "legacy"), [ ({"name": "foo"}, True), # irrelevant key ({"requires-python": ">=3.9"}, False), ({"dependencies": ["bar"]}, False), ({"dependencies": []}, False), # relevant even if empty ({"optional-dependencies": "..."}, False), ], ) @pytest.mark.parametrize( "local_config", [ {}, # empty {"dependencies": [uuid.uuid4().hex]}, # only legacy { "dependencies": [uuid.uuid4().hex], "group": [uuid.uuid4().hex], }, # legacy and new ], ) def test_content_hash_with_project_section( project: dict[str, Any], local_config: dict[str, list[str]], legacy: bool, locker: Locker, ) -> None: """Legacy generation if there is no project section.""" def _get_legacy_content_hash() -> str: relevant_content = {} for key in locker._relevant_keys: data = local_config.get(key) if data is None and key not in locker._legacy_keys: continue relevant_content[key] = data return sha256(json.dumps(relevant_content, sort_keys=True).encode()).hexdigest() locker = locker.__class__( lock=locker.lock, pyproject_data={"project": project, "tool": {"poetry": local_config}}, ) old_content_hash = _get_legacy_content_hash() content_hash = locker._get_content_hash() if legacy: assert content_hash == old_content_hash else: assert content_hash != old_content_hash @pytest.mark.parametrize( ("groups", "legacy"), [ ({}, True), ({"foo": []}, False), ], ) @pytest.mark.parametrize( "project", [ {"name": "foo"}, # irrelevant key {"requires-python": ">=3.9"}, # relevant key ], ) @pytest.mark.parametrize( "local_config", [ {}, # empty {"dependencies": [uuid.uuid4().hex]}, # only legacy { "dependencies": [uuid.uuid4().hex], "group": [uuid.uuid4().hex], }, # legacy and new ], ) def test_content_hash_with_dependency_groups_section( groups: dict[str, Any], project: dict[str, Any], local_config: dict[str, Any], legacy: bool, locker: Locker, ) -> None: """Legacy generation if there is no dependency-groups section.""" def _get_legacy_content_hash() -> str: project_content = project tool_poetry_content = local_config relevant_project_content = {} for key in locker._relevant_project_keys: data = project_content.get(key) if data is not None: relevant_project_content[key] = data relevant_poetry_content: dict[str, Any] = {} for key in locker._relevant_keys: data = tool_poetry_content.get(key) if data is None and ( # Special handling for legacy keys is just for backwards compatibility, # and thereby not required if there is relevant content in [project]. key not in locker._legacy_keys or relevant_project_content ): continue relevant_poetry_content[key] = data if relevant_project_content: relevant_content = { "project": relevant_project_content, "tool": {"poetry": relevant_poetry_content}, } else: # For backwards compatibility, we have to put the relevant content # of the [tool.poetry] section at top level! relevant_content = relevant_poetry_content return sha256(json.dumps(relevant_content, sort_keys=True).encode()).hexdigest() locker = locker.__class__( lock=locker.lock, pyproject_data={ "project": project, "dependency-groups": groups, "tool": {"poetry": local_config}, }, ) old_content_hash = _get_legacy_content_hash() content_hash = locker._get_content_hash() if legacy: assert content_hash == old_content_hash else: assert content_hash != old_content_hash def test_lock_file_resolves_file_url_symlinks( root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: """ Create directories and file structure as follows: d1/ d1/testsymlink -> d1/d2/d3 d1/d2/d3/lock_file d1/d4/source_file Using the testsymlink as the Locker.lock file path should correctly resolve to the real physical path of the source_file when calculating the relative path from the lock_file, i.e. "../../d4/source_file" instead of the unresolved path from the symlink itself which would have been "../d4/source_file" See https://github.com/python-poetry/poetry/issues/5849 """ with tempfile.TemporaryDirectory() as d1: symlink_path = Path(d1).joinpath("testsymlink") with ( tempfile.TemporaryDirectory(dir=d1) as d2, tempfile.TemporaryDirectory(dir=d1) as d4, tempfile.TemporaryDirectory(dir=d2) as d3, tempfile.NamedTemporaryFile(dir=d4) as source_file, tempfile.NamedTemporaryFile(dir=d3) as lock_file, ): lock_file.close() try: os.symlink(Path(d3), symlink_path) except OSError: if sys.platform == "win32": # os.symlink requires either administrative privileges or developer # mode on Win10, throwing an OSError if neither is active. # Test is not possible in that case. return raise locker = Locker(symlink_path / lock_file.name, {}) package_local = Package( "local-package", "1.2.3", source_type="file", source_url=source_file.name, source_reference="develop", source_resolved_reference="123456", ) packages = { package_local: transitive_info, } locker.set_lock_data(root, packages) with locker.lock.open(encoding="utf-8") as f: content = f.read() expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "local-package" version = "1.2.3" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.source] type = "file" url = "{ Path( os.path.relpath( Path(source_file.name).resolve().as_posix(), Path(Path(lock_file.name).parent).resolve().as_posix(), ) ).as_posix() }" reference = "develop" resolved_reference = "123456" [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ assert content == expected def test_lockfile_is_not_rewritten_if_only_poetry_version_changed( locker: Locker, root: ProjectPackage ) -> None: generated_comment_old_version = GENERATED_COMMENT.replace(__version__, "1.3.2") assert generated_comment_old_version != GENERATED_COMMENT old_content = f"""\ # {generated_comment_old_version} package = [] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with open(locker.lock, "w", encoding="utf-8") as f: f.write(old_content) assert not locker.set_lock_data(root, {}) with locker.lock.open(encoding="utf-8") as f: content = f.read() assert content == old_content def test_lockfile_keep_eol( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: sep = "\n" if os.linesep == "\r\n" else "\r\n" with open(locker.lock, "wb") as f: f.write((sep * 10).encode()) packages = {Package("test", version="0.0.1"): transitive_info} assert locker.set_lock_data(root, packages) with locker.lock.open(encoding="utf-8", newline="") as f: line, *_ = f.read().splitlines(keepends=True) if sep == "\r\n": assert line.endswith("\r\n") else: assert not line.endswith("\r\n") def test_lock_file_dependency_constraints_are_ordered_deterministically( locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: """Dependency constraints for the same package should be sorted by name and marker to ensure deterministic lock file output regardless of the order they are added.""" package_a = get_package("A", "1.0.0") # Add dependencies on B in non-sorted order (by marker and version) package_a.add_dependency( Factory.create_dependency( "B", {"version": ">=2.0", "markers": 'sys_platform == "win32"'}, ) ) package_a.add_dependency( Factory.create_dependency( "B", {"version": ">=1.0", "markers": 'sys_platform == "linux"'}, ) ) locker.set_lock_data(root, {package_a: transitive_info}) expected = f"""\ # {GENERATED_COMMENT} [[package]] name = "A" version = "1.0.0" description = "" optional = false python-versions = "*" groups = ["main"] files = [] [package.dependencies] B = [ {{version = ">=1.0", markers = "sys_platform == \\"linux\\""}}, {{version = ">=2.0", markers = "sys_platform == \\"win32\\""}}, ] [metadata] lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ with locker.lock.open(encoding="utf-8") as f: content = f.read() assert content == expected # Run again to verify idempotency locker.set_lock_data(root, {package_a: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content2 = f.read() assert content2 == expected ================================================ FILE: tests/packages/test_transitive_package_info.py ================================================ from __future__ import annotations import pytest from packaging.utils import canonicalize_name from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.version.markers import parse_marker from poetry.packages.transitive_package_info import TransitivePackageInfo DEV_GROUP = canonicalize_name("dev") @pytest.mark.parametrize( "groups, expected", [ (set(), ""), ({"main"}, 'sys_platform == "linux"'), ({"dev"}, 'python_version < "3.9"'), ({"main", "dev"}, 'sys_platform == "linux" or python_version < "3.9"'), ({"foo"}, ""), ({"main", "foo", "dev"}, 'sys_platform == "linux" or python_version < "3.9"'), ], ) def test_get_marker(groups: set[str], expected: str) -> None: info = TransitivePackageInfo( depth=0, groups={MAIN_GROUP, DEV_GROUP}, markers={ MAIN_GROUP: parse_marker('sys_platform =="linux"'), DEV_GROUP: parse_marker('python_version < "3.9"'), }, ) assert str(info.get_marker([canonicalize_name(g) for g in groups])) == expected ================================================ FILE: tests/plugins/__init__.py ================================================ ================================================ FILE: tests/plugins/test_plugin_manager.py ================================================ from __future__ import annotations import shutil import sys from pathlib import Path from typing import TYPE_CHECKING from typing import ClassVar from typing import Protocol import pytest from cleo.io.buffered_io import BufferedIO from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.factory import Factory from poetry.installation.wheel_installer import WheelInstaller from poetry.packages.locker import Locker from poetry.plugins import ApplicationPlugin from poetry.plugins import Plugin from poetry.plugins.plugin_manager import PluginManager from poetry.plugins.plugin_manager import ProjectPluginCache from poetry.poetry import Poetry from poetry.puzzle.exceptions import SolverProblemError from poetry.repositories import Repository from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository from tests.helpers import mock_metadata_entry_points if TYPE_CHECKING: from cleo.io.io import IO from pytest_mock import MockerFixture from poetry.console.commands.command import Command from poetry.utils.env import Env from tests.conftest import Config from tests.types import FixtureDirGetter class ManagerFactory(Protocol): def __call__(self, group: str = Plugin.group) -> PluginManager: ... class MyPlugin(Plugin): def activate(self, poetry: Poetry, io: IO) -> None: io.write_line("Setting readmes") poetry.package.readmes = (Path("README.md"),) class MyCommandPlugin(ApplicationPlugin): commands: ClassVar[list[type[Command]]] = [] class InvalidPlugin: group = "poetry.plugin" def activate(self, poetry: Poetry, io: IO) -> None: io.write_line("Updating version") poetry.package.version = Version.parse("9.9.9") @pytest.fixture def repo() -> Repository: repo = Repository("repo") repo.add_package(Package("my-other-plugin", "1.0")) for version in ("1.0", "2.0"): package = Package("my-application-plugin", version) package.add_dependency(Dependency("some-lib", version)) repo.add_package(package) repo.add_package(Package("some-lib", version)) return repo @pytest.fixture def pool(repo: Repository) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(repo) return pool @pytest.fixture def poetry(fixture_dir: FixtureDirGetter, config: Config) -> Poetry: project_path = fixture_dir("simple_project") poetry = Poetry( project_path / "pyproject.toml", {}, ProjectPackage("simple-project", "1.2.3"), Locker(project_path / "poetry.lock", {}), config, ) return poetry @pytest.fixture def poetry_with_plugins( fixture_dir: FixtureDirGetter, pool: RepositoryPool, tmp_path: Path ) -> Poetry: orig_path = fixture_dir("project_plugins") project_path = tmp_path / "project" project_path.mkdir() shutil.copy(orig_path / "pyproject.toml", project_path / "pyproject.toml") poetry = Factory().create_poetry(project_path) poetry.set_pool(pool) return poetry @pytest.fixture() def io() -> BufferedIO: return BufferedIO() @pytest.fixture(autouse=True) def mock_sys_path(mocker: MockerFixture) -> None: sys_path_copy = sys.path.copy() mocker.patch("poetry.plugins.plugin_manager.sys.path", new=sys_path_copy) @pytest.fixture() def manager_factory(poetry: Poetry, io: BufferedIO) -> ManagerFactory: def _manager(group: str = Plugin.group) -> PluginManager: return PluginManager(group) return _manager @pytest.fixture def with_my_plugin(mocker: MockerFixture) -> None: mock_metadata_entry_points(mocker, MyPlugin) @pytest.fixture def with_invalid_plugin(mocker: MockerFixture) -> None: mock_metadata_entry_points(mocker, InvalidPlugin) def test_load_plugins_and_activate( manager_factory: ManagerFactory, poetry: Poetry, io: BufferedIO, with_my_plugin: None, ) -> None: manager = manager_factory() manager.load_plugins() manager.activate(poetry, io) assert poetry.package.readmes == (Path("README.md"),) assert io.fetch_output() == "Setting readmes\n" def test_load_plugins_with_invalid_plugin( manager_factory: ManagerFactory, poetry: Poetry, io: BufferedIO, with_invalid_plugin: None, ) -> None: manager = manager_factory() with pytest.raises(ValueError): manager.load_plugins() def test_add_project_plugin_path( poetry_with_plugins: Poetry, io: BufferedIO, system_env: Env, fixture_dir: FixtureDirGetter, ) -> None: dist_info_1 = "my_application_plugin-1.0.dist-info" dist_info_2 = "my_application_plugin-2.0.dist-info" cache = ProjectPluginCache(poetry_with_plugins, io) shutil.copytree( fixture_dir("project_plugins") / dist_info_1, cache._path / dist_info_1 ) shutil.copytree( fixture_dir("project_plugins") / dist_info_2, system_env.purelib / dist_info_2 ) assert { f"{p.name} {p.version}" for p in InstalledRepository.load(system_env).packages } == {"my-application-plugin 2.0"} PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent) assert { f"{p.name} {p.version}" for p in InstalledRepository.load(system_env).packages } == {"my-application-plugin 1.0"} def test_add_project_plugin_path_addsitedir_called( poetry_with_plugins: Poetry, io: BufferedIO, mocker: MockerFixture, ) -> None: """Test that addsitedir is called when plugin path exists.""" cache = ProjectPluginCache(poetry_with_plugins, io) cache._path.mkdir(parents=True, exist_ok=True) mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir") PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent) # sys.path is mocked, so we can check it was modified assert str(cache._path) in sys.path assert sys.path[0] == str(cache._path) mock_addsitedir.assert_called_once_with(str(cache._path)) def test_add_project_plugin_path_no_addsitedir_when_path_missing( poetry_with_plugins: Poetry, mocker: MockerFixture, ) -> None: """Test that addsitedir is not called when plugin path doesn't exist.""" cache = ProjectPluginCache(poetry_with_plugins, BufferedIO()) # Ensure the plugin path does not exist if cache._path.exists(): shutil.rmtree(cache._path) mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir") initial_sys_path = sys.path.copy() PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent) assert sys.path == initial_sys_path mock_addsitedir.assert_not_called() def test_add_project_plugin_path_no_pyproject( tmp_path: Path, mocker: MockerFixture, ) -> None: """Test that no action is taken when pyproject.toml is missing.""" mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir") initial_sys_path = sys.path.copy() # Call with a directory that has no pyproject.toml PluginManager.add_project_plugin_path(tmp_path) assert sys.path == initial_sys_path mock_addsitedir.assert_not_called() def test_ensure_plugins_no_plugins_no_output(poetry: Poetry, io: BufferedIO) -> None: PluginManager.ensure_project_plugins(poetry, io) assert not (poetry.pyproject_path.parent / ProjectPluginCache.PATH).exists() assert io.fetch_output() == "" assert io.fetch_error() == "" def test_ensure_plugins_no_plugins_existing_cache_is_removed( poetry: Poetry, io: BufferedIO ) -> None: plugin_path = poetry.pyproject_path.parent / ProjectPluginCache.PATH plugin_path.mkdir(parents=True) PluginManager.ensure_project_plugins(poetry, io) assert not plugin_path.exists() assert io.fetch_output() == ( "No project plugins defined. Removing the project's plugin cache\n\n" ) assert io.fetch_error() == "" @pytest.mark.parametrize("debug_out", [False, True]) def test_ensure_plugins_no_output_if_fresh( poetry_with_plugins: Poetry, io: BufferedIO, debug_out: bool ) -> None: io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) cache = ProjectPluginCache(poetry_with_plugins, io) cache._write_config() cache.ensure_plugins() assert cache._config_file.exists() assert ( cache._gitignore_file.exists() and cache._gitignore_file.read_text(encoding="utf-8") == "*" ) assert io.fetch_output() == ( "The project's plugin cache is up to date.\n\n" if debug_out else "" ) assert io.fetch_error() == "" @pytest.mark.parametrize("debug_out", [False, True]) def test_ensure_plugins_ignore_irrelevant_markers( poetry_with_plugins: Poetry, io: BufferedIO, debug_out: bool ) -> None: io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) poetry_with_plugins.local_config["requires-plugins"] = { "irrelevant": {"version": "1.0", "markers": "python_version < '3'"} } cache = ProjectPluginCache(poetry_with_plugins, io) cache.ensure_plugins() assert cache._config_file.exists() assert ( cache._gitignore_file.exists() and cache._gitignore_file.read_text(encoding="utf-8") == "*" ) assert io.fetch_output() == ( "No relevant project plugins for Poetry's environment defined.\n\n" if debug_out else "" ) assert io.fetch_error() == "" def test_ensure_plugins_remove_outdated( poetry_with_plugins: Poetry, io: BufferedIO, fixture_dir: FixtureDirGetter ) -> None: # Test with irrelevant plugins because this is the first return # where it is relevant that an existing cache is removed. poetry_with_plugins.local_config["requires-plugins"] = { "irrelevant": {"version": "1.0", "markers": "python_version < '3'"} } fixture_path = fixture_dir("project_plugins") cache = ProjectPluginCache(poetry_with_plugins, io) cache._path.mkdir(parents=True) dist_info = "my_application_plugin-1.0.dist-info" shutil.copytree(fixture_path / dist_info, cache._path / dist_info) cache._config_file.touch() cache.ensure_plugins() assert cache._config_file.exists() assert not (cache._path / dist_info).exists() assert io.fetch_output() == ( "Removing the project's plugin cache because it is outdated\n" ) assert io.fetch_error() == "" def test_ensure_plugins_ignore_already_installed_in_system_env( poetry_with_plugins: Poetry, io: BufferedIO, system_env: Env, fixture_dir: FixtureDirGetter, ) -> None: fixture_path = fixture_dir("project_plugins") for dist_info in ( "my_application_plugin-2.0.dist-info", "my_other_plugin-1.0.dist-info", ): shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) cache = ProjectPluginCache(poetry_with_plugins, io) cache.ensure_plugins() assert cache._config_file.exists() assert ( cache._gitignore_file.exists() and cache._gitignore_file.read_text(encoding="utf-8") == "*" ) assert io.fetch_output() == ( "Ensuring that the Poetry plugins required by the project are available...\n" "All required plugins have already been installed in Poetry's environment.\n\n" ) assert io.fetch_error() == "" def test_ensure_plugins_install_missing_plugins( poetry_with_plugins: Poetry, io: BufferedIO, system_env: Env, fixture_dir: FixtureDirGetter, mocker: MockerFixture, ) -> None: cache = ProjectPluginCache(poetry_with_plugins, io) install_spy = mocker.spy(cache, "_install") execute_mock = mocker.patch( "poetry.plugins.plugin_manager.Installer._execute", return_value=0 ) cache.ensure_plugins() install_spy.assert_called_once_with( [ Dependency("my-application-plugin", ">=2.0"), Dependency("my-other-plugin", ">=1.0"), ], system_env, [], ) execute_mock.assert_called_once() assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ "", "", "", ] assert cache._config_file.exists() assert ( cache._gitignore_file.exists() and cache._gitignore_file.read_text(encoding="utf-8") == "*" ) assert io.fetch_output() == ( "Ensuring that the Poetry plugins required by the project are available...\n" "The following Poetry plugins are required by the project" " but are not installed in Poetry's environment:\n" " - my-application-plugin (>=2.0)\n" " - my-other-plugin (>=1.0)\n" "Installing Poetry plugins only for the current project...\n" "Updating dependencies\n" "Resolving dependencies...\n\n" "Writing lock file\n\n" ) assert io.fetch_error() == "" def test_ensure_plugins_install_only_missing_plugins( poetry_with_plugins: Poetry, io: BufferedIO, system_env: Env, fixture_dir: FixtureDirGetter, mocker: MockerFixture, ) -> None: fixture_path = fixture_dir("project_plugins") for dist_info in ( "my_application_plugin-2.0.dist-info", "some_lib-2.0.dist-info", ): shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) cache = ProjectPluginCache(poetry_with_plugins, io) install_spy = mocker.spy(cache, "_install") execute_mock = mocker.patch( "poetry.plugins.plugin_manager.Installer._execute", return_value=0 ) cache.ensure_plugins() install_spy.assert_called_once_with( [Dependency("my-other-plugin", ">=1.0")], system_env, [Package("my-application-plugin", "2.0"), Package("some-lib", "2.0")], ) execute_mock.assert_called_once() assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ "" ] assert cache._config_file.exists() assert ( cache._gitignore_file.exists() and cache._gitignore_file.read_text(encoding="utf-8") == "*" ) assert io.fetch_output() == ( "Ensuring that the Poetry plugins required by the project are available...\n" "The following Poetry plugins are required by the project" " but are not installed in Poetry's environment:\n" " - my-other-plugin (>=1.0)\n" "Installing Poetry plugins only for the current project...\n" "Updating dependencies\n" "Resolving dependencies...\n\n" "Writing lock file\n\n" ) assert io.fetch_error() == "" @pytest.mark.parametrize("debug_out", [False, True]) def test_ensure_plugins_install_overwrite_wrong_version_plugins( poetry_with_plugins: Poetry, io: BufferedIO, system_env: Env, fixture_dir: FixtureDirGetter, mocker: MockerFixture, debug_out: bool, ) -> None: io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) fixture_path = fixture_dir("project_plugins") for dist_info in ( "my_application_plugin-1.0.dist-info", "some_lib-2.0.dist-info", ): shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) cache = ProjectPluginCache(poetry_with_plugins, io) install_spy = mocker.spy(cache, "_install") execute_mock = mocker.patch( "poetry.plugins.plugin_manager.Installer._execute", return_value=0 ) cache.ensure_plugins() install_spy.assert_called_once_with( [ Dependency("my-application-plugin", ">=2.0"), Dependency("my-other-plugin", ">=1.0"), ], system_env, [Package("some-lib", "2.0")], ) execute_mock.assert_called_once() assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ "", "", ] assert cache._config_file.exists() assert ( cache._gitignore_file.exists() and cache._gitignore_file.read_text(encoding="utf-8") == "*" ) start = ( "Ensuring that the Poetry plugins required by the project are available...\n" ) opt = ( "The following Poetry plugins are required by the project" " but are not satisfied by the installed versions:\n" " - my-application-plugin (>=2.0)\n" " installed: my-application-plugin (1.0)\n" ) end = ( "The following Poetry plugins are required by the project" " but are not installed in Poetry's environment:\n" " - my-application-plugin (>=2.0)\n" " - my-other-plugin (>=1.0)\n" "Installing Poetry plugins only for the current project...\n" ) expected = (start + opt + end) if debug_out else (start + end) assert io.fetch_output().startswith(expected) assert io.fetch_error() == "" def test_ensure_plugins_pins_other_installed_packages( poetry_with_plugins: Poetry, io: BufferedIO, system_env: Env, fixture_dir: FixtureDirGetter, mocker: MockerFixture, ) -> None: fixture_path = fixture_dir("project_plugins") for dist_info in ( "my_application_plugin-1.0.dist-info", "some_lib-1.0.dist-info", ): shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) cache = ProjectPluginCache(poetry_with_plugins, io) install_spy = mocker.spy(cache, "_install") execute_mock = mocker.patch( "poetry.plugins.plugin_manager.Installer._execute", return_value=0 ) with pytest.raises(SolverProblemError): cache.ensure_plugins() install_spy.assert_called_once_with( [ Dependency("my-application-plugin", ">=2.0"), Dependency("my-other-plugin", ">=1.0"), ], system_env, # pinned because it might be a dependency of another plugin or Poetry itself [Package("some-lib", "1.0")], ) execute_mock.assert_not_called() assert not cache._config_file.exists() assert ( cache._gitignore_file.exists() and cache._gitignore_file.read_text(encoding="utf-8") == "*" ) assert io.fetch_output() == ( "Ensuring that the Poetry plugins required by the project are available...\n" "The following Poetry plugins are required by the project" " but are not installed in Poetry's environment:\n" " - my-application-plugin (>=2.0)\n" " - my-other-plugin (>=1.0)\n" "Installing Poetry plugins only for the current project...\n" "Updating dependencies\n" "Resolving dependencies...\n" ) assert io.fetch_error() == "" @pytest.mark.parametrize("other_version", [False, True]) def test_project_plugins_are_installed_in_project_folder( poetry_with_plugins: Poetry, io: BufferedIO, system_env: Env, fixture_dir: FixtureDirGetter, tmp_path: Path, other_version: bool, ) -> None: orig_purelib = system_env.purelib orig_platlib = system_env.platlib # make sure that the path dependency is on the same drive (for Windows tests in CI) orig_wheel_path = ( fixture_dir("wheel_with_no_requires_dist") / "demo-0.1.0-py2.py3-none-any.whl" ) wheel_path = tmp_path / orig_wheel_path.name shutil.copy(orig_wheel_path, wheel_path) if other_version: WheelInstaller(system_env).install(wheel_path) dist_info = orig_purelib / "demo-0.1.0.dist-info" metadata = dist_info / "METADATA" metadata.write_text( metadata.read_text(encoding="utf-8").replace("0.1.0", "0.1.2"), encoding="utf-8", ) dist_info.rename(orig_purelib / "demo-0.1.2.dist-info") cache = ProjectPluginCache(poetry_with_plugins, io) # just use a file dependency so that we do not have to set up a repository cache._install([FileDependency("demo", wheel_path)], system_env, []) project_site_packages = [p.name for p in cache._path.iterdir()] assert "demo" in project_site_packages assert "demo-0.1.0.dist-info" in project_site_packages orig_site_packages = [p.name for p in orig_purelib.iterdir()] if other_version: assert "demo" in orig_site_packages assert "demo-0.1.2.dist-info" in orig_site_packages assert "demo-0.1.0.dist-info" not in orig_site_packages else: assert not any(p.startswith("demo") for p in orig_site_packages) if orig_platlib != orig_purelib: assert not any(p.name.startswith("demo") for p in orig_platlib.iterdir()) ================================================ FILE: tests/publishing/__init__.py ================================================ ================================================ FILE: tests/publishing/test_hash_manager.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any import pytest from poetry.publishing.hash_manager import HashManager if TYPE_CHECKING: from pathlib import Path from pytest_mock import MockerFixture from tests.types import FixtureDirGetter @pytest.fixture def distributions_dir(fixture_dir: FixtureDirGetter) -> Path: return fixture_dir("distributions") @pytest.mark.parametrize( "file, hashes", ( ( "demo-0.1.0.tar.gz", ( "d1912c917363a64e127318655f7d1fe7", "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", "cb638093d63df647e70b03e963bedc31e021cb088695e29101b69f525e3d5fef", ), ), ( "demo-0.1.2-py2.py3-none-any.whl", ( "53b4e10d2bfa81a4206221c4b87843d9", "55dde4e6828081de7a1e429f33180459c333d9da593db62a3d75a8f5e505dde1", "b35b9aab064e88fffe42309550ebe425907fb42ccb3b1d173b7d6b7509f38eac", ), ), ), ) def test_file_hashes_returns_proper_hashes_for_file( file: str, hashes: tuple[str, ...], distributions_dir: Path ) -> None: manager = HashManager() manager.hash(distributions_dir / file) file_hashes = manager.hexdigest() assert file_hashes == hashes def test_file_hashes_returns_none_for_md5_with_fips( mocker: MockerFixture, distributions_dir: Path ) -> None: # disable md5 def fips_md5(*args: Any, **kwargs: Any) -> Any: raise ValueError("Disabled by FIPS") mocker.patch("hashlib.md5", new=fips_md5) manager = HashManager() manager.hash(distributions_dir / "demo-0.1.0.tar.gz") file_hashes = manager.hexdigest() assert file_hashes.md5 is None def test_file_hashes_returns_none_for_blake2_with_fips( mocker: MockerFixture, distributions_dir: Path ) -> None: # disable md5 def fips_blake2b(*args: Any, **kwargs: Any) -> Any: raise ValueError("Disabled by FIPS") mocker.patch("hashlib.blake2b", new=fips_blake2b) manager = HashManager() manager.hash(distributions_dir / "demo-0.1.0.tar.gz") file_hashes = manager.hexdigest() assert file_hashes.blake2_256 is None ================================================ FILE: tests/publishing/test_publisher.py ================================================ from __future__ import annotations import os from pathlib import Path from typing import TYPE_CHECKING import pytest from cleo.io.buffered_io import BufferedIO from cleo.io.null_io import NullIO from packaging.utils import canonicalize_name from poetry.factory import Factory from poetry.publishing.publisher import Publisher if TYPE_CHECKING: from pytest_mock import MockerFixture from tests.conftest import Config from tests.types import FixtureDirGetter def test_publish_publishes_to_pypi_by_default( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config ) -> None: uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth") uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") poetry = Factory().create_poetry(fixture_dir("sample_project")) poetry._config = config poetry.config.merge( {"http-basic": {"pypi": {"username": "foo", "password": "bar"}}} ) publisher = Publisher(poetry, NullIO()) publisher.publish(None, None, None) assert uploader_auth.call_args == [("foo", "bar")] assert uploader_upload.call_args == [ ("https://upload.pypi.org/legacy/",), {"cert": True, "client_cert": None, "dry_run": False, "skip_existing": False}, ] @pytest.mark.parametrize("fixture_name", ["sample_project", "with_source"]) def test_publish_can_publish_to_given_repository( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config, fixture_name: str, ) -> None: uploader_version = "1.2.3+test" mocker.patch("poetry.publishing.uploader.Uploader.version", uploader_version) uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth") uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") config.merge( { "repositories": {"foo": {"url": "http://foo.bar"}}, "http-basic": {"foo": {"username": "foo", "password": "bar"}}, } ) mocker.patch("poetry.config.config.Config.create", return_value=config) poetry = Factory().create_poetry(fixture_dir(fixture_name)) # Normally both versions are equal, but we want to check that the correct version # is displayed in the output if they are different. assert poetry.package.version != uploader_version io = BufferedIO() publisher = Publisher(poetry, io) publisher.publish("foo", None, None) assert uploader_auth.call_args == [("foo", "bar")] assert uploader_upload.call_args == [ ("http://foo.bar",), {"cert": True, "client_cert": None, "dry_run": False, "skip_existing": False}, ] project_name = canonicalize_name(fixture_name) assert f"Publishing {project_name} ({uploader_version}) to foo" in io.fetch_output() def test_publish_raises_error_for_undefined_repository( fixture_dir: FixtureDirGetter, config: Config ) -> None: poetry = Factory().create_poetry(fixture_dir("sample_project")) poetry._config = config poetry.config.merge( {"http-basic": {"my-repo": {"username": "foo", "password": "bar"}}} ) publisher = Publisher(poetry, NullIO()) with pytest.raises(RuntimeError): publisher.publish("my-repo", None, None) def assert_publish_uses_token_if_it_exists( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config | None = None ) -> None: uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth") uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") poetry = Factory().create_poetry(fixture_dir("sample_project")) if config: poetry._config = config publisher = Publisher(poetry, NullIO()) publisher.publish(None, None, None) assert uploader_auth.call_args == [("__token__", "my-token")] assert uploader_upload.call_args == [ ("https://upload.pypi.org/legacy/",), {"cert": True, "client_cert": None, "dry_run": False, "skip_existing": False}, ] def test_publish_uses_token_if_it_exists( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config ) -> None: config.merge({"pypi-token": {"pypi": "my-token"}}) assert_publish_uses_token_if_it_exists(fixture_dir, mocker, config) def test_publish_uses_env_token_if_it_exists( fixture_dir: FixtureDirGetter, mocker: MockerFixture, environ: None ) -> None: os.environ["POETRY_PYPI_TOKEN_PYPI"] = "my-token" assert_publish_uses_token_if_it_exists(fixture_dir, mocker) def test_publish_uses_cert( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config ) -> None: cert = "path/to/ca.pem" uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth") uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") poetry = Factory().create_poetry(fixture_dir("sample_project")) poetry._config = config poetry.config.merge( { "repositories": {"foo": {"url": "https://foo.bar"}}, "http-basic": {"foo": {"username": "foo", "password": "bar"}}, "certificates": {"foo": {"cert": cert}}, } ) publisher = Publisher(poetry, NullIO()) publisher.publish("foo", None, None) assert uploader_auth.call_args == [("foo", "bar")] assert uploader_upload.call_args == [ ("https://foo.bar",), { "cert": Path(cert), "client_cert": None, "dry_run": False, "skip_existing": False, }, ] def test_publish_uses_client_cert( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config ) -> None: client_cert = "path/to/client.pem" uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") poetry = Factory().create_poetry(fixture_dir("sample_project")) poetry._config = config poetry.config.merge( { "repositories": {"foo": {"url": "https://foo.bar"}}, "certificates": {"foo": {"client-cert": client_cert}}, } ) publisher = Publisher(poetry, NullIO()) publisher.publish("foo", None, None) assert uploader_upload.call_args == [ ("https://foo.bar",), { "cert": True, "client_cert": Path(client_cert), "dry_run": False, "skip_existing": False, }, ] def test_publish_read_from_environment_variable( fixture_dir: FixtureDirGetter, environ: None, mocker: MockerFixture, config: Config, ) -> None: os.environ["POETRY_REPOSITORIES_FOO_URL"] = "https://foo.bar" os.environ["POETRY_HTTP_BASIC_FOO_USERNAME"] = "bar" os.environ["POETRY_HTTP_BASIC_FOO_PASSWORD"] = "baz" uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth") uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") poetry = Factory().create_poetry(fixture_dir("sample_project")) publisher = Publisher(poetry, NullIO()) publisher.publish("foo", None, None) assert uploader_auth.call_args == [("bar", "baz")] assert uploader_upload.call_args == [ ("https://foo.bar",), {"cert": True, "client_cert": None, "dry_run": False, "skip_existing": False}, ] ================================================ FILE: tests/publishing/test_uploader.py ================================================ from __future__ import annotations import shutil from typing import TYPE_CHECKING import pytest from cleo.io.null_io import NullIO from poetry.factory import Factory from poetry.publishing.uploader import Uploader from poetry.publishing.uploader import UploadError if TYPE_CHECKING: from pathlib import Path import responses from pytest_mock import MockerFixture from poetry.poetry import Poetry from tests.types import FixtureDirGetter @pytest.fixture def poetry(fixture_dir: FixtureDirGetter) -> Poetry: return Factory().create_poetry(fixture_dir("simple_project")) @pytest.fixture def uploader(poetry: Poetry) -> Uploader: return Uploader(poetry, NullIO()) @pytest.mark.parametrize( ("files", "expected_files", "expected_version"), [ ([], [], ""), ( ["simple_project-1.2.3.tar.gz", "simple_project-1.2.3-py3-none-any.whl"], ["simple_project-1.2.3.tar.gz", "simple_project-1.2.3-py3-none-any.whl"], "1.2.3", ), ( # other names are ignored [ "simple_project-1.2.3.tar.gz", "other_project-1.2.3.tar.gz", "simple_project-1.2.3-py3-none-any.whl", "other_project-1.2.3-py3-none-any.whl", ], ["simple_project-1.2.3.tar.gz", "simple_project-1.2.3-py3-none-any.whl"], "1.2.3", ), ( # older versions are ignored [ "simple_project-1.2.3.tar.gz", "simple_project-1.2.4.tar.gz", "simple_project-1.2.3-py3-none-any.whl", "simple_project-1.2.4-py3-none-any.whl", ], ["simple_project-1.2.4.tar.gz", "simple_project-1.2.4-py3-none-any.whl"], "1.2.4", ), ( # older versions are ignored - only new sdist [ "simple_project-1.2.3.tar.gz", "simple_project-1.2.4.tar.gz", "simple_project-1.2.3-py3-none-any.whl", ], ["simple_project-1.2.4.tar.gz"], "1.2.4", ), ( # older versions are ignored - only new wheel [ "simple_project-1.2.3.tar.gz", "simple_project-1.2.3-py3-none-any.whl", "simple_project-1.2.4-py3-none-any.whl", ], ["simple_project-1.2.4-py3-none-any.whl"], "1.2.4", ), ( # older versions are ignored - local version [ "simple_project-1.2.3.tar.gz", "simple_project-1.2.3+hash1.tar.gz", "simple_project-1.2.3+hash2.tar.gz", "simple_project-1.2.3-py3-none-any.whl", "simple_project-1.2.3+hash1-py3-none-any.whl", "simple_project-1.2.3+hash2-py3-none-any.whl", ], [ "simple_project-1.2.3+hash2.tar.gz", "simple_project-1.2.3+hash2-py3-none-any.whl", ], "1.2.3+hash2", ), ( # older versions are ignore - pre-release [ "simple_project-1.2.3rc1.tar.gz", "simple_project-1.2.3rc1-py3-none-any.whl", "simple_project-1.2.3.tar.gz", "simple_project-1.2.3-py3-none-any.whl", ], [ "simple_project-1.2.3.tar.gz", "simple_project-1.2.3-py3-none-any.whl", ], "1.2.3", ), ], ) def test_uploader_files_only_latest( poetry: Poetry, tmp_path: Path, files: list[str], expected_files: list[str], expected_version: str, ) -> None: for file in files: (tmp_path / file).touch() uploader = Uploader(poetry, NullIO(), dist_dir=tmp_path) assert uploader.files == [tmp_path / f for f in expected_files] assert uploader.version == expected_version def test_uploader_properly_handles_400_errors( http: responses.RequestsMock, uploader: Uploader ) -> None: http.post("https://foo.com", status=400, body="Bad request") with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") assert str(e.value) == "HTTP Error 400: Bad Request | b'Bad request'" def test_uploader_properly_handles_403_errors( http: responses.RequestsMock, uploader: Uploader ) -> None: http.post("https://foo.com", status=403, body="Unauthorized") with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") assert str(e.value) == "HTTP Error 403: Forbidden | b'Unauthorized'" def test_uploader_properly_handles_nonstandard_errors( http: responses.RequestsMock, uploader: Uploader ) -> None: # content based off a true story. # Message changed to protect the ~~innocent~~ guilty. content = ( b'{\n "errors": [ {\n ' b'"status": 400,' b'"message": "I cant let you do that, dave"\n' b"} ]\n}" ) http.post("https://foo.com", status=400, body=content) with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") assert str(e.value) == f"HTTP Error 400: Bad Request | {content!r}" @pytest.mark.parametrize( ("status", "code"), [ (308, "Permanent Redirect"), (307, "Temporary Redirect"), (304, "Not Modified"), (303, "See Other"), (302, "Found"), (301, "Moved Permanently"), (300, "Multiple Choices"), ], ) def test_uploader_properly_handles_redirects( http: responses.RequestsMock, uploader: Uploader, status: int, code: str ) -> None: http.post("https://foo.com", status=status) with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") assert ( str(e.value) == "Redirects are not supported. Is the URL missing a trailing slash?" ) def test_uploader_properly_handles_301_redirects( http: responses.RequestsMock, uploader: Uploader ) -> None: http.post("https://foo.com", status=301, body="Redirect") with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") assert ( str(e.value) == "Redirects are not supported. Is the URL missing a trailing slash?" ) def test_uploader_registers_with_sdist_for_appropriate_400_errors( http: responses.RequestsMock, uploader: Uploader ) -> None: http.post("https://foo.com", status=400, body="No package was ever registered") with pytest.raises(UploadError): uploader.upload("https://foo.com") assert len(http.calls) == 2 bodies = [c.request.body or b"" for c in http.calls] assert b'name=":action"\r\n\r\nfile_upload\r\n' in bodies[0] assert b'name=":action"\r\n\r\nsubmit\r\n' in bodies[1] assert b"sdist" in bodies[0] assert b"sdist" in bodies[1] assert b"bdist_wheel" not in bodies[0] assert b"bdist_wheel" not in bodies[1] def test_uploader_register_uses_wheel_if_no_sdist( http: responses.RequestsMock, poetry: Poetry, tmp_path: Path ) -> None: dist_dir = tmp_path / "dist" dist_dir.mkdir() shutil.copy( poetry.file.path.parent / "dist" / "simple_project-1.2.3-py2.py3-none-any.whl", dist_dir, ) uploader = Uploader(poetry, NullIO(), dist_dir=dist_dir) http.post("https://foo.com", status=400, body="No package was ever registered") with pytest.raises(UploadError): uploader.upload("https://foo.com") assert len(http.calls) == 2 bodies = [c.request.body or b"" for c in http.calls] assert b'name=":action"\r\n\r\nfile_upload\r\n' in bodies[0] assert b'name=":action"\r\n\r\nsubmit\r\n' in bodies[1] assert b"sdist" not in bodies[0] assert b"sdist" not in bodies[1] assert b"bdist_wheel" in bodies[0] assert b"bdist_wheel" in bodies[1] @pytest.mark.parametrize( "status, body", [ (409, ""), (400, "File already exists"), (400, "Repository does not allow updating assets"), (400, "cannot be updated"), (403, "Not enough permissions to overwrite artifact"), (400, "file name has already been taken"), ], ) def test_uploader_skips_existing( http: responses.RequestsMock, uploader: Uploader, status: int, body: str ) -> None: http.post("https://foo.com", status=status, body=body) # should not raise uploader.upload("https://foo.com", skip_existing=True) def test_uploader_skip_existing_bubbles_unskippable_errors( http: responses.RequestsMock, uploader: Uploader ) -> None: http.post("https://foo.com", status=403, body="Unauthorized") with pytest.raises(UploadError): uploader.upload("https://foo.com", skip_existing=True) def test_uploader_properly_handles_file_not_existing( mocker: MockerFixture, http: responses.RequestsMock, uploader: Uploader ) -> None: mocker.patch("pathlib.Path.is_file", return_value=False) with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") assert f"Archive ({uploader.files[0]}) does not exist" == str(e.value) def test_uploader_post_data_wheel(fixture_dir: FixtureDirGetter) -> None: file = ( fixture_dir("simple_project") / "dist" / "simple_project-1.2.3-py2.py3-none-any.whl" ) assert Uploader.post_data(file) == { "md5_digest": "fb4a5266406b9cf34ceaa88d1c8b7a01", "sha256_digest": "fc365a242d4de8b8661babc088f44b3df25e9e0017ef5dd7140dfe50f9323e16", "blake2_256_digest": "2e006d1fbfef0ed38fbded1ec1614dc4fd66f81061fe290528e2744dbc25ce31", "filetype": "bdist_wheel", "pyversion": "py2.py3", "metadata_version": "2.1", "name": "simple-project", "version": "1.2.3", "summary": "Some description.", "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "license": "MIT", "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ], "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*", "description": "My Package\n==========\n\n", "description_content_type": "text/x-rst", "keywords": "packaging, dependency, poetry", "home_page": "https://poetry.eustace.io", "project_urls": [ "Documentation, https://poetry.eustace.io/docs", "Repository, https://github.com/sdispater/poetry", ], } def test_uploader_post_data_sdist(fixture_dir: FixtureDirGetter) -> None: file = fixture_dir("simple_project") / "dist" / "simple_project-1.2.3.tar.gz" assert Uploader.post_data(file) == { "md5_digest": "e611cbb8f31258243d90f7681dfda68a", "sha256_digest": "c4a72becabca29ec2a64bf8c820bbe204d2268f53e102501ea5605bc1c1675d1", "blake2_256_digest": "d3df22f4944f6acd02105e7e2df61ef63c7b0f4337a12df549ebc2805a13c2be", "filetype": "sdist", "pyversion": "source", "metadata_version": "2.1", "name": "simple-project", "version": "1.2.3", "summary": "Some description.", "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ], "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*", "keywords": "packaging, dependency, poetry", "home_page": "https://poetry.eustace.io", "project_urls": [ "Documentation, https://poetry.eustace.io/docs", "Repository, https://github.com/sdispater/poetry", ], } ================================================ FILE: tests/puzzle/__init__.py ================================================ ================================================ FILE: tests/puzzle/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.io.null_io import NullIO from poetry.core.packages.project_package import ProjectPackage from poetry.puzzle import Solver from poetry.repositories import Repository from poetry.repositories import RepositoryPool from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import mock_clone if TYPE_CHECKING: from pytest_mock import MockerFixture @pytest.fixture(autouse=True) def setup(mocker: MockerFixture) -> None: # Patch git module to not actually clone projects mocker.patch("poetry.vcs.git.Git.clone", new=mock_clone) p = mocker.patch("poetry.vcs.git.Git.get_revision") p.return_value = MOCK_DEFAULT_GIT_REVISION @pytest.fixture def io() -> NullIO: return NullIO() @pytest.fixture def package() -> ProjectPackage: return ProjectPackage("root", "1.0") @pytest.fixture def repo() -> Repository: return Repository("repo") @pytest.fixture def pool(repo: Repository) -> RepositoryPool: return RepositoryPool([repo]) @pytest.fixture def solver(package: ProjectPackage, pool: RepositoryPool, io: NullIO) -> Solver: return Solver(package, pool, [], [], io) ================================================ FILE: tests/puzzle/test_provider.py ================================================ from __future__ import annotations import shutil from pathlib import Path from subprocess import CalledProcessError from typing import TYPE_CHECKING from typing import Any import pytest from cleo.io.null_io import NullIO from packaging.utils import NormalizedName from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency from poetry.core.packages.directory_dependency import DirectoryDependency from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.vcs_dependency import VCSDependency from poetry.factory import Factory from poetry.inspection.info import PackageInfo from poetry.packages import DependencyPackage from poetry.puzzle.provider import IncompatibleConstraintsError from poetry.puzzle.provider import Provider from poetry.repositories.cached_repository import CachedRepository from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.repository import Repository from poetry.repositories.repository_pool import Priority from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import EnvCommandError from poetry.utils.env import MockEnv as BaseMockEnv from tests.helpers import get_dependency if TYPE_CHECKING: from pathlib import Path from poetry.core.constraints.version import Version from pytest_mock import MockerFixture from tests.types import FixtureDirGetter SOME_URL = "https://example.com/path.tar.gz" class MockEnv(BaseMockEnv): def run(self, bin: str, *args: str, **kwargs: Any) -> str: raise EnvCommandError(CalledProcessError(1, "python", output="")) class MockCachedRepository(CachedRepository): def _get_release_info( self, name: NormalizedName, version: Version ) -> dict[str, Any]: raise NotImplementedError def package(self, name: str, version: Version) -> Package: package = super().package(name, version) package._source_reference = self.name return package @pytest.fixture def root() -> ProjectPackage: return ProjectPackage("root", "1.2.3") @pytest.fixture def repository() -> Repository: return Repository("repo") @pytest.fixture def pool(repository: Repository) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(repository) return pool @pytest.fixture def provider(root: ProjectPackage, pool: RepositoryPool) -> Provider: return Provider(root, pool, NullIO()) @pytest.fixture def release_info() -> PackageInfo: return PackageInfo( name="mylib", version="1.0", summary="", requires_dist=[], requires_python=">=3.9", files=[ { "file": "mylib-1.0-py3-none-any.whl", "hash": "sha256:dummyhashvalue1234567890abcdef", }, { "file": "mylib-1.0.tar.gz", "hash": "sha256:anotherdummyhashvalueabcdef1234567890", }, ], cache_version=str(CachedRepository.CACHE_VERSION), ) @pytest.fixture def release_info_complete() -> PackageInfo: return PackageInfo( name="mylib", version="1.0", summary="", requires_dist=[], requires_python=">=3.9", files=[ { "file": "mylib-1.0-py3-none-any.whl", "hash": "sha256:dummyhashvalue1234567890abcdef", "url": "https://example.org/mylib-1.0-py3-none-any.whl", "size": 12345, "upload_time": "2024-01-01T12:00:00Z", }, { "file": "mylib-1.0.tar.gz", "hash": "sha256:anotherdummyhashvalueabcdef1234567890", "url": "https://example.org/mylib-1.0.tar.gz", "size": 12346, "upload_time": "2024-01-01T12:00:01Z", }, ], cache_version=str(CachedRepository.CACHE_VERSION), ) @pytest.fixture def outdated_release_info() -> PackageInfo: return PackageInfo( name="mylib", version="1.0", summary="", requires_dist=[], requires_python=">=3.9", files=[ { "file": "mylib-1.0-py3-none-any.whl", "hash": "sha256:dummyhashvalue1234567890abcdef", } ], cache_version=str(CachedRepository.CACHE_VERSION), ) @pytest.mark.parametrize( "dependency, expected", [ (Dependency("foo", "<2"), [Package("foo", "1")]), (Dependency("foo", "<2", extras=["bar"]), [Package("foo", "1")]), (Dependency("foo", ">=1"), [Package("foo", "2"), Package("foo", "1")]), (Dependency("foo", ">=1a"), [Package("foo", "2"), Package("foo", "1")]), ( Dependency("foo", ">=1", allows_prereleases=True), [ Package("foo", "3a"), Package("foo", "2"), Package("foo", "2a"), Package("foo", "1"), ], ), ], ) def test_search_for( provider: Provider, repository: Repository, dependency: Dependency, expected: list[Package], ) -> None: foo1 = Package("foo", "1") foo2a = Package("foo", "2a") foo2 = Package("foo", "2") foo3a = Package("foo", "3a") repository.add_package(foo1) repository.add_package(foo2a) repository.add_package(foo2) repository.add_package(foo3a) assert provider.search_for(dependency) == expected @pytest.mark.parametrize( "dependency, direct_origin_dependency, expected_before, expected_after", [ ( Dependency("foo", ">=1"), URLDependency("foo", SOME_URL), [Package("foo", "3")], [Package("foo", "2a", source_type="url", source_url=SOME_URL)], ), ( Dependency("foo", ">=2"), URLDependency("foo", SOME_URL), [Package("foo", "3")], [Package("foo", "3")], ), ( Dependency("foo", ">=1", extras=["bar"]), URLDependency("foo", SOME_URL), [Package("foo", "3")], [Package("foo", "2a", source_type="url", source_url=SOME_URL)], ), ( Dependency("foo", ">=1"), URLDependency("foo", SOME_URL, extras=["baz"]), [Package("foo", "3")], [Package("foo", "2a", source_type="url", source_url=SOME_URL)], ), ( Dependency("foo", ">=1", extras=["bar"]), URLDependency("foo", SOME_URL, extras=["baz"]), [Package("foo", "3")], [Package("foo", "2a", source_type="url", source_url=SOME_URL)], ), ], ) def test_search_for_direct_origin_and_extras( provider: Provider, repository: Repository, mocker: MockerFixture, dependency: Dependency, direct_origin_dependency: Dependency, expected_before: list[Package], expected_after: list[Package], ) -> None: foo2a_direct_origin = Package("foo", "2a", source_type="url", source_url=SOME_URL) mocker.patch( "poetry.puzzle.provider.Provider.search_for_direct_origin_dependency", return_value=foo2a_direct_origin, ) foo2a = Package("foo", "2a") foo3 = Package("foo", "3") repository.add_package(foo2a) repository.add_package(foo3) assert provider.search_for(dependency) == expected_before assert provider.search_for(direct_origin_dependency) == [foo2a_direct_origin] assert provider.search_for(dependency) == expected_after @pytest.mark.parametrize("value", [True, False]) def test_search_for_vcs_retains_develop_flag(provider: Provider, value: bool) -> None: dependency = VCSDependency( "demo", "git", "https://github.com/demo/demo.git", develop=value ) package = provider.search_for_direct_origin_dependency(dependency) assert package.develop == value def test_search_for_vcs_setup_egg_info(provider: Provider) -> None: dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git") package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_vcs_setup_egg_info_with_extras(provider: Provider) -> None: dependency = VCSDependency( "demo", "git", "https://github.com/demo/demo.git", extras=["foo"] ) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_vcs_read_setup(provider: Provider, mocker: MockerFixture) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git") package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_vcs_read_setup_with_extras( provider: Provider, mocker: MockerFixture ) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) dependency = VCSDependency( "demo", "git", "https://github.com/demo/demo.git", extras=["foo"] ) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} def test_search_for_vcs_read_setup_raises_error_if_no_version( provider: Provider, mocker: MockerFixture ) -> None: mocker.patch( "poetry.inspection.info.get_pep517_metadata", return_value=PackageInfo(name="demo", version=None), ) dependency = VCSDependency("demo", "git", "https://github.com/demo/no-version.git") with pytest.raises(RuntimeError): provider.search_for_direct_origin_dependency(dependency) @pytest.mark.parametrize("directory", ["demo", "non-canonical-name"]) def test_search_for_directory_setup_egg_info( provider: Provider, directory: str, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: path = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / directory, tmp_path / "project" ) dependency = DirectoryDependency("demo", path) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_directory_setup_egg_info_with_extras( provider: Provider, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: path = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project" ) dependency = DirectoryDependency("demo", path, extras=["foo"]) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } @pytest.mark.parametrize("directory", ["demo", "non-canonical-name"]) def test_search_for_directory_setup_with_base( provider: Provider, directory: str, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: path = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / directory, tmp_path / "project" ) dependency = DirectoryDependency("demo", path, base=path) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } assert package.root_dir == path def test_search_for_directory_setup_read_setup( provider: Provider, mocker: MockerFixture, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) path = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project" ) dependency = DirectoryDependency("demo", path) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_directory_setup_read_setup_with_extras( provider: Provider, mocker: MockerFixture, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) path = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project" ) dependency = DirectoryDependency("demo", path, extras=["foo"]) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" required = {r for r in package.requires if not r.is_optional()} optional = {r for r in package.requires if r.is_optional()} assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == {get_dependency("tomlkit"), get_dependency("cleo")} assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_directory_setup_read_setup_with_no_dependencies( provider: Provider, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: path = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / "no-dependencies", tmp_path / "project", ) dependency = DirectoryDependency("demo", path) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.2" assert package.requires == [] assert package.extras == {} def test_search_for_directory_poetry( provider: Provider, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: path = shutil.copytree(fixture_dir("project_with_extras"), tmp_path / "project") dependency = DirectoryDependency("project-with-extras", path) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "project-with-extras" assert package.version.text == "1.2.3" required = { r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional() } optional = { r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional() } assert not required assert optional == { get_dependency("cachy", ">=0.2.0"), get_dependency("pendulum", ">=1.4.4"), } extras_a = canonicalize_name("extras-a") extras_b = canonicalize_name("extras-b") assert set(package.extras) == {extras_a, extras_b} assert set(package.extras[extras_a]) == {get_dependency("pendulum", ">=1.4.4")} assert set(package.extras[extras_b]) == {get_dependency("cachy", ">=0.2.0")} def test_search_for_directory_poetry_with_extras( provider: Provider, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: path = shutil.copytree(fixture_dir("project_with_extras"), tmp_path / "project") dependency = DirectoryDependency("project-with-extras", path, extras=["extras_a"]) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "project-with-extras" assert package.version.text == "1.2.3" required = { r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional() } optional = { r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional() } assert not required assert optional == { get_dependency("cachy", ">=0.2.0"), get_dependency("pendulum", ">=1.4.4"), } extras_a = canonicalize_name("extras-a") extras_b = canonicalize_name("extras-b") assert set(package.extras) == {extras_a, extras_b} assert set(package.extras[extras_a]) == {get_dependency("pendulum", ">=1.4.4")} assert set(package.extras[extras_b]) == {get_dependency("cachy", ">=0.2.0")} def test_search_for_file_sdist( provider: Provider, fixture_dir: FixtureDirGetter ) -> None: dependency = FileDependency( "demo", fixture_dir("distributions") / "demo-0.1.0.tar.gz", ) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.0" assert package.files == [ { "file": "demo-0.1.0.tar.gz", "hash": "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", "size": 1003, } ] required = { r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional() } optional = { r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional() } assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == { get_dependency("cleo"), get_dependency("tomlkit"), } assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_file_sdist_with_extras( provider: Provider, fixture_dir: FixtureDirGetter ) -> None: dependency = FileDependency( "demo", fixture_dir("distributions") / "demo-0.1.0.tar.gz", extras=["foo"], ) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.0" assert package.files == [ { "file": "demo-0.1.0.tar.gz", "hash": "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", "size": 1003, } ] required = { r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional() } optional = { r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional() } assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == { get_dependency("cleo"), get_dependency("tomlkit"), } assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_file_wheel( provider: Provider, fixture_dir: FixtureDirGetter ) -> None: dependency = FileDependency( "demo", fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl", ) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.0" assert package.files == [ { "file": "demo-0.1.0-py2.py3-none-any.whl", "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", "size": 1116, } ] required = { r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional() } optional = { r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional() } assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == { get_dependency("cleo"), get_dependency("tomlkit"), } assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_search_for_file_wheel_with_extras( provider: Provider, fixture_dir: FixtureDirGetter ) -> None: dependency = FileDependency( "demo", fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl", extras=["foo"], ) package = provider.search_for_direct_origin_dependency(dependency) assert package.name == "demo" assert package.version.text == "0.1.0" assert package.files == [ { "file": "demo-0.1.0-py2.py3-none-any.whl", "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", "size": 1116, } ] required = { r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional() } optional = { r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional() } assert required == {get_dependency("pendulum", ">=1.4.4")} assert optional == { get_dependency("cleo"), get_dependency("tomlkit"), } assert package.extras == { "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } def test_complete_package_merges_same_source_and_no_source( provider: Provider, root: ProjectPackage ) -> None: foo_no_source_1 = get_dependency("foo", ">=1") foo_source_1 = get_dependency("foo", "!=1.1.*") foo_source_1.source_name = "source" foo_source_2 = get_dependency("foo", "!=1.2.*") foo_source_2.source_name = "source" foo_no_source_2 = get_dependency("foo", "<2") root.add_dependency(foo_no_source_1) root.add_dependency(foo_source_1) root.add_dependency(foo_source_2) root.add_dependency(foo_no_source_2) complete_package = provider.complete_package( DependencyPackage(root.to_dependency(), root) ) requires = complete_package.package.all_requires assert len(requires) == 1 assert requires[0].source_name == "source" assert str(requires[0].constraint) in { ">=1,<1.1 || >=1.3,<2", ">=1,<1.1.dev0 || >=1.3.dev0,<2", ">=1,<1.1.0 || >=1.3.0,<2", ">=1,<1.1.0.dev0 || >=1.3.0.dev0,<2", } def test_complete_package_does_not_merge_different_source_names( provider: Provider, root: ProjectPackage ) -> None: foo_source_1 = get_dependency("foo") foo_source_1.source_name = "source_1" foo_source_2 = get_dependency("foo") foo_source_2.source_name = "source_2" root.add_dependency(foo_source_1) root.add_dependency(foo_source_2) with pytest.raises(IncompatibleConstraintsError) as e: provider.complete_package(DependencyPackage(root.to_dependency(), root)) expected = """\ Incompatible constraints in requirements of root (1.2.3): foo ; source=source_2 foo ; source=source_1""" assert str(e.value) == expected def test_complete_package_merges_same_source_type_and_no_source( provider: Provider, root: ProjectPackage, fixture_dir: FixtureDirGetter ) -> None: project_dir = fixture_dir("with_conditional_path_deps") path = (project_dir / "demo_one").as_posix() root.add_dependency(Factory.create_dependency("demo", ">=1.0")) root.add_dependency(Factory.create_dependency("demo", {"path": path})) root.add_dependency(Factory.create_dependency("demo", {"path": path})) # duplicate root.add_dependency(Factory.create_dependency("demo", "<2.0")) complete_package = provider.complete_package( DependencyPackage(root.to_dependency(), root) ) requires = complete_package.package.all_requires assert len(requires) == 1 assert requires[0].source_url == path assert str(requires[0].constraint) == "1.2.3" def test_complete_package_does_not_merge_different_source_types( provider: Provider, root: ProjectPackage, fixture_dir: FixtureDirGetter ) -> None: project_dir = fixture_dir("with_conditional_path_deps") for folder in ["demo_one", "demo_two"]: path = (project_dir / folder).as_posix() root.add_dependency(Factory.create_dependency("demo", {"path": path})) with pytest.raises(IncompatibleConstraintsError) as e: provider.complete_package(DependencyPackage(root.to_dependency(), root)) expected = f"""\ Incompatible constraints in requirements of root (1.2.3): demo @ {project_dir.as_uri()}/demo_two (1.2.3) demo @ {project_dir.as_uri()}/demo_one (1.2.3)""" assert str(e.value) == expected def test_complete_package_does_not_merge_different_source_type_and_name( provider: Provider, root: ProjectPackage, fixture_dir: FixtureDirGetter ) -> None: project_dir = fixture_dir("with_conditional_path_deps") path = (project_dir / "demo_one").as_posix() dep_with_source_name = Factory.create_dependency("demo", ">=1.0") dep_with_source_name.source_name = "source" root.add_dependency(dep_with_source_name) root.add_dependency(Factory.create_dependency("demo", {"path": path})) with pytest.raises(IncompatibleConstraintsError) as e: provider.complete_package(DependencyPackage(root.to_dependency(), root)) expected = f"""\ Incompatible constraints in requirements of root (1.2.3): demo @ {project_dir.as_uri()}/demo_one (1.2.3) demo (>=1.0) ; source=source""" assert str(e.value) == expected def test_complete_package_does_not_merge_different_subdirectories( provider: Provider, root: ProjectPackage ) -> None: dependency_one = Factory.create_dependency( "one", { "git": "https://github.com/demo/subdirectories.git", "subdirectory": "one", }, ) dependency_one_copy = Factory.create_dependency( "one", { "git": "https://github.com/demo/subdirectories.git", "subdirectory": "one-copy", }, ) root.add_dependency(dependency_one) root.add_dependency(dependency_one_copy) with pytest.raises(IncompatibleConstraintsError) as e: provider.complete_package(DependencyPackage(root.to_dependency(), root)) expected = """\ Incompatible constraints in requirements of root (1.2.3): one @ git+https://github.com/demo/subdirectories.git#subdirectory=one-copy (1.0.0) one @ git+https://github.com/demo/subdirectories.git#subdirectory=one (1.0.0)""" assert str(e.value) == expected @pytest.mark.parametrize("source_name", [None, "repo"]) def test_complete_package_with_extras_preserves_source_name( provider: Provider, repository: Repository, source_name: str | None ) -> None: package_a = Package("A", "1.0") package_b = Package("B", "1.0") dep = get_dependency("B", "^1.0", optional=True) package_a.add_dependency(dep) package_a.extras = {canonicalize_name("foo"): [dep]} repository.add_package(package_a) repository.add_package(package_b) dependency = Dependency("A", "1.0", extras=["foo"]) if source_name: dependency.source_name = source_name complete_package = provider.complete_package( DependencyPackage(dependency, package_a) ) requires = complete_package.package.all_requires assert len(requires) == 2 assert requires[0].name == "a" assert requires[0].source_name == source_name assert requires[1].name == "b" assert requires[1].source_name is None @pytest.mark.parametrize("with_extra", [False, True]) def test_complete_package_fetches_optional_vcs_dependency_only_if_requested( provider: Provider, repository: Repository, mocker: MockerFixture, with_extra: bool ) -> None: optional_vcs_dependency = Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git", "optional": True} ) package = Package("A", "1.0", features=["foo"] if with_extra else []) package.add_dependency(optional_vcs_dependency) package.extras = {canonicalize_name("foo"): [optional_vcs_dependency]} repository.add_package(package) spy = mocker.spy(provider, "_search_for_vcs") provider.complete_package(DependencyPackage(package.to_dependency(), package)) if with_extra: spy.assert_called() else: spy.assert_not_called() def test_complete_package_finds_locked_package_in_explicit_source( root: ProjectPackage, pool: RepositoryPool ) -> None: package = Package("a", "1.0", source_reference="explicit") explicit_repo = Repository("explicit") explicit_repo.add_package(package) pool.add_repository(explicit_repo, priority=Priority.EXPLICIT) root_dependency = get_dependency("a", ">0") root_dependency.source_name = "explicit" root.add_dependency(root_dependency) locked_package = Package("a", "1.0", source_reference="explicit") provider = Provider(root, pool, NullIO(), locked=[locked_package]) provider.complete_package(DependencyPackage(root.to_dependency(), root)) # transitive dependency without explicit source dependency = get_dependency("a", ">=1") locked = provider.get_locked(dependency) assert locked is not None provider.complete_package(locked) # must not fail def test_complete_package_finds_locked_package_in_other_source( root: ProjectPackage, repository: Repository, pool: RepositoryPool ) -> None: package = Package("a", "1.0") repository.add_package(package) explicit_repo = Repository("explicit") pool.add_repository(explicit_repo) root_dependency = get_dependency("a", ">0") # no explicit source root.add_dependency(root_dependency) locked_package = Package("a", "1.0", source_reference="explicit") # explicit source provider = Provider(root, pool, NullIO(), locked=[locked_package]) provider.complete_package(DependencyPackage(root.to_dependency(), root)) # transitive dependency without explicit source dependency = get_dependency("a", ">=1") locked = provider.get_locked(dependency) assert locked is not None provider.complete_package(locked) # must not fail def test_complete_package_raises_packagenotfound_if_locked_source_not_available( root: ProjectPackage, pool: RepositoryPool, provider: Provider ) -> None: locked_package = Package("a", "1.0", source_reference="outdated") provider = Provider(root, pool, NullIO(), locked=[locked_package]) provider.complete_package(DependencyPackage(root.to_dependency(), root)) # transitive dependency without explicit source dependency = get_dependency("a", ">=1") locked = provider.get_locked(dependency) assert locked is not None with pytest.raises(PackageNotFoundError): provider.complete_package(locked) def test_complete_package_outdated_cache( root: ProjectPackage, release_info: PackageInfo, outdated_release_info: PackageInfo, mocker: MockerFixture, ) -> None: repo = MockCachedRepository("repo") pool = RepositoryPool() pool.add_repository(repo) provider = Provider(root, pool, NullIO()) pool_refresh_spy = mocker.spy(provider.pool, "refresh") assert release_info.name is not None assert release_info.version is not None package = Package(release_info.name, release_info.version) package.files = release_info.files # up-to-date files from lock # trigger caching of outdated info repo._get_release_info = lambda name, version: outdated_release_info.asdict() # type: ignore[method-assign] assert len(repo.package(package.name, package.version).files) == 1 # additional files uploaded -> refresh needed repo._get_release_info = lambda name, version: release_info.asdict() # type: ignore[method-assign] complete_package = provider.complete_package( DependencyPackage(package.to_dependency(), package) ) assert len(complete_package.package.files) == 2 assert pool_refresh_spy.call_count == 1 # cache should have been updated -> no additional refresh needed complete_package = provider.complete_package( DependencyPackage(package.to_dependency(), package) ) assert len(complete_package.package.files) == 2 assert pool_refresh_spy.call_count == 1 def test_complete_package_no_refresh_on_url_size_upload_info( root: ProjectPackage, release_info: PackageInfo, release_info_complete: PackageInfo, mocker: MockerFixture, ) -> None: repo = MockCachedRepository("repo") pool = RepositoryPool() pool.add_repository(repo) provider = Provider(root, pool, NullIO()) pool_refresh_spy = mocker.spy(provider.pool, "refresh") assert release_info.name is not None assert release_info.version is not None package = Package(release_info.name, release_info.version) package.files = release_info.files # up-to-date files from lock assert "url" not in package.files[0] # trigger caching complete info repo._get_release_info = lambda name, version: release_info_complete.asdict() # type: ignore[method-assign] assert len(repo.package(package.name, package.version).files) == 2 # Only additional information in cache, names and hashes from lock file are the same # -> no refresh needed complete_package = provider.complete_package( DependencyPackage(package.to_dependency(), package) ) assert len(complete_package.package.files) == 2 assert "url" in complete_package.package.files[0] assert pool_refresh_spy.call_count == 0 def test_source_dependency_is_satisfied_by_direct_origin( provider: Provider, repository: Repository ) -> None: direct_origin_package = Package("foo", "1.1", source_type="url") repository.add_package(Package("foo", "1.0")) provider._direct_origin_packages = {"foo": direct_origin_package} dep = Dependency("foo", ">=1") assert provider.search_for(dep) == [direct_origin_package] def test_explicit_source_dependency_is_not_satisfied_by_direct_origin( provider: Provider, repository: Repository ) -> None: repo_package = Package("foo", "1.0") repository.add_package(repo_package) provider._direct_origin_packages = {"foo": Package("foo", "1.1", source_type="url")} dep = Dependency("foo", ">=1") dep.source_name = repository.name assert provider.search_for(dep) == [repo_package] def test_source_dependency_is_not_satisfied_by_incompatible_direct_origin( provider: Provider, repository: Repository ) -> None: repo_package = Package("foo", "2.0") repository.add_package(repo_package) provider._direct_origin_packages = {"foo": Package("foo", "1.0", source_type="url")} dep = Dependency("foo", ">=2") dep.source_name = repository.name assert provider.search_for(dep) == [repo_package] ================================================ FILE: tests/puzzle/test_solver.py ================================================ from __future__ import annotations import re import shutil import sys from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Literal import pytest from cleo.io.buffered_io import BufferedIO from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.dependency_group import DependencyGroup from poetry.core.packages.package import Package from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.version.markers import parse_marker from poetry.factory import Factory from poetry.installation.operations import Update from poetry.packages import DependencyPackage from poetry.puzzle import Solver from poetry.puzzle.exceptions import SolverProblemError from poetry.puzzle.provider import IncompatibleConstraintsError from poetry.repositories.repository import Repository from poetry.repositories.repository_pool import Priority from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import MockEnv from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import get_dependency from tests.helpers import get_package if TYPE_CHECKING: import responses from cleo.io.null_io import NullIO from poetry.core.packages.project_package import ProjectPackage from pytest_mock import MockerFixture from poetry.installation.operations.operation import Operation from poetry.puzzle.provider import Provider from poetry.puzzle.transaction import Transaction from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pypi_repository import PyPiRepository from tests.types import FixtureDirGetter from tests.types import PackageFactory DEFAULT_SOURCE_REF = ( VCSDependency("poetry", "git", "git@github.com:python-poetry/poetry.git").branch or "HEAD" ) @pytest.fixture def legacy_repository(legacy_repository_html: LegacyRepository) -> LegacyRepository: """ Override fixture to only test with the html version of the legacy repository because the json version has the same packages as the PyPI repository and thus cause different results in the tests that rely on differences. """ return legacy_repository_html def set_package_python_versions(provider: Provider, python_versions: str) -> None: provider._package.python_versions = python_versions provider._package_python_constraint = provider._package.python_constraint def check_solver_result( transaction: Transaction, expected: list[dict[str, Any]], synchronize: bool = False, ) -> list[Operation]: for e in expected: if "skipped" not in e: e["skipped"] = False result = [] ops = transaction.calculate_operations(synchronize=synchronize) for op in ops: if op.job_type == "update": assert isinstance(op, Update) result.append( { "job": "update", "from": op.initial_package, "to": op.target_package, "skipped": op.skipped, } ) else: job = "install" if op.job_type == "uninstall": job = "remove" result.append({"job": job, "package": op.package, "skipped": op.skipped}) assert result == expected return ops def test_solver_install_single( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") repo.add_package(package_a) transaction = solver.solve([get_dependency("A").name]) check_solver_result(transaction, [{"job": "install", "package": package_a}]) def test_solver_remove_if_no_longer_locked( package: ProjectPackage, pool: RepositoryPool, io: NullIO ) -> None: package_a = get_package("A", "1.0") solver = Solver(package, pool, [package_a], [package_a], io) transaction = solver.solve() check_solver_result(transaction, [{"job": "remove", "package": package_a}]) def test_remove_non_installed( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: package_a = get_package("A", "1.0") repo.add_package(package_a) solver = Solver(package, pool, [], [package_a], io) transaction = solver.solve([]) check_solver_result(transaction, []) def test_install_non_existing_package_fail( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("B", "1")) package_a = get_package("A", "1.0") repo.add_package(package_a) with pytest.raises(SolverProblemError): solver.solve() def test_install_unpublished_package_fails( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: package.add_dependency(Factory.create_dependency("B", "1")) package_a = get_package("A", "1.0") package_b = get_package("B", "1") package_b.add_dependency(Factory.create_dependency("A", "1.0")) repo.add_package(package_a) # Even though B is installed, it is unpublished and cannot be used during solving. solver = Solver(package, pool, [package_b], [], io) with pytest.raises(SolverProblemError): solver.solve() def test_solver_with_deps( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") new_package_b = get_package("B", "1.1") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(new_package_b) package_a.add_dependency(get_dependency("B", "<1.1")) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) def test_install_honours_not_equal( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") new_package_b11 = get_package("B", "1.1") new_package_b12 = get_package("B", "1.2") new_package_b13 = get_package("B", "1.3") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(new_package_b11) repo.add_package(new_package_b12) repo.add_package(new_package_b13) package_a.add_dependency(get_dependency("B", "<=1.3,!=1.3,!=1.2")) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": new_package_b11}, {"job": "install", "package": package_a}, ], ) def test_install_with_deps_in_order( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency(Factory.create_dependency("C", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) package_b.add_dependency(get_dependency("A", ">=1.0")) package_b.add_dependency(get_dependency("C", ">=1.0")) package_c.add_dependency(get_dependency("A", ">=1.0")) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a}, {"job": "install", "package": package_c}, {"job": "install", "package": package_b}, ], ) def test_install_installed( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") repo.add_package(package_a) solver = Solver(package, pool, [package_a], [], io) transaction = solver.solve() check_solver_result( transaction, [{"job": "install", "package": package_a, "skipped": True}] ) def test_update_installed( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") new_package_a = get_package("A", "1.1") repo.add_package(package_a) repo.add_package(new_package_a) solver = Solver(package, pool, [get_package("A", "1.0")], [], io) transaction = solver.solve() check_solver_result( transaction, [{"job": "update", "from": package_a, "to": new_package_a}] ) def test_update_with_use_latest( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package_a = get_package("A", "1.0") new_package_a = get_package("A", "1.1") package_b = get_package("B", "1.0") new_package_b = get_package("B", "1.1") repo.add_package(package_a) repo.add_package(new_package_a) repo.add_package(package_b) repo.add_package(new_package_b) installed = [get_package("A", "1.0")] locked = [package_a, package_b] solver = Solver(package, pool, installed, locked, io) transaction = solver.solve(use_latest=[package_b.name]) check_solver_result( transaction, [ {"job": "install", "package": package_a, "skipped": True}, {"job": "install", "package": new_package_b}, ], ) def test_solver_sets_groups( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*", groups=["dev"])) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_b.add_dependency(Factory.create_dependency("C", "~1.0")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) transaction = solver.solve() _ = check_solver_result( transaction, [ {"job": "install", "package": package_c}, {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ], ) def test_solver_respects_root_package_python_versions( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~3.4") package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_b.python_versions = "^3.3" package_c = get_package("C", "1.0") package_c.python_versions = "^3.4" package_c11 = get_package("C", "1.1") package_c11.python_versions = "^3.6" package_b.add_dependency(Factory.create_dependency("C", "^1.0")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_c11) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_c}, {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ], ) def test_solver_fails_if_mismatch_root_python_versions( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "^3.4") package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_b.python_versions = "^3.6" package_c = get_package("C", "1.0") package_c.python_versions = "~3.3" package_b.add_dependency(Factory.create_dependency("C", "~1.0")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) with pytest.raises(SolverProblemError): solver.solve() def test_solver_ignores_python_restricted_if_mismatch_root_package_python_versions( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~3.8") package.add_dependency( Factory.create_dependency("A", {"version": "1.0", "python": "<3.8"}) ) package.add_dependency( Factory.create_dependency( "B", {"version": "1.0", "markers": "python_version < '3.8'"} ) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") repo.add_package(package_a) repo.add_package(package_b) transaction = solver.solve() check_solver_result(transaction, []) def test_solver_solves_optional_and_compatible_packages( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~3.4") package.extras = {canonicalize_name("foo"): [get_dependency("B")]} package.add_dependency( Factory.create_dependency("A", {"version": "*", "python": "^3.4"}) ) package.add_dependency( Factory.create_dependency("B", {"version": "*", "optional": True}) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_b.python_versions = "^3.3" package_c = get_package("C", "1.0") package_c.python_versions = "^3.4" package_b.add_dependency(Factory.create_dependency("C", "^1.0")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_c}, {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ], ) def test_solver_does_not_return_extras_if_not_requested( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_b.extras = {canonicalize_name("foo"): [get_dependency("C", "^1.0")]} repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ], ) def test_solver_returns_extras_if_requested( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency( Factory.create_dependency("B", {"version": "*", "extras": ["foo"]}) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") dep = get_dependency("C", "^1.0", optional=True) dep.marker = parse_marker("extra == 'foo'") package_b.extras = {canonicalize_name("foo"): [dep]} package_b.add_dependency(dep) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": package_c}, {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ], ) assert ops[-1].package.marker.is_any() assert ops[0].package.marker.is_any() @pytest.mark.parametrize("num_groups", [0, 1, 2]) def test_solver_returns_extras_if_requested_in_multiple_groups( solver: Solver, repo: Repository, package: ProjectPackage, num_groups: int ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) if num_groups: package.add_dependency( Factory.create_dependency( "B", {"version": "*", "extras": ["foo"]}, groups=[f"group{i}" for i in range(num_groups)], ) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") dep = get_dependency("C", "^1.0", optional=True) dep.marker = parse_marker("extra == 'foo'") package_b.extras = {canonicalize_name("foo"): [dep]} package_b.add_dependency(dep) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) transaction = solver.solve() expected = [ {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ] if num_groups: expected = [{"job": "install", "package": package_c}, *expected] ops = check_solver_result(transaction, expected) assert ops[-1].package.marker.is_any() assert ops[0].package.marker.is_any() @pytest.mark.parametrize( ("enabled_extras", "expected_packages"), [ ([], ["a"]), (["all"], ["download-package", "install-package", "a"]), (["nested"], ["download-package", "install-package", "a"]), (["cyclic"], ["download-package", "install-package", "a"]), (["install", "download"], ["download-package", "install-package", "a"]), (["install"], ["install-package", "a"]), (["download"], ["download-package", "a"]), # test to ensure target extra dependencies with markers are respected (["py"], ["py310-package", "a"]), ], ) @pytest.mark.parametrize("merge_extras", [True, False]) @pytest.mark.parametrize("top_level_dependency", [True, False]) def test_solver_resolves_self_referential_extras( enabled_extras: list[str], expected_packages: list[Literal["a", "b", "download-package", "install-package"]], top_level_dependency: bool, solver: Solver, repo: Repository, package: ProjectPackage, create_package: PackageFactory, merge_extras: bool, ) -> None: dependency = ( create_package( "A", str(package.version), extras={ "download": ["download-package"], "download2": ["download-package"], # same package as download "install": ["install-package"], "py38": ["py38-package ; python_version == '3.8'"], "py310": ["py310-package ; python_version > '3.8'"], "all": ["a[download,download2,install]"], "py": ["a[py38,py310]"], "nested": ["a[all]"], "cyclic": ["a[cyclic2]", "download-package"], "cyclic2": ["a[cyclic]", "install-package"], }, merge_extras=merge_extras, ) .to_dependency() .with_features(enabled_extras) ) if not top_level_dependency: dependency = create_package( "B", "1.0", dependencies=[dependency] ).to_dependency() # we do not use append() here to avoid flaky tests expected_packages = [*expected_packages, "b"] package.add_dependency(dependency) # Solving the dependency graph with solver.use_environment(MockEnv((3, 10, 0))): transaction = solver.solve() # Verifying the results check_solver_result( transaction, [ {"job": "install", "package": repo.package(name, package.version)} for name in expected_packages ], ) def test_solver_resolves_self_referential_extras_markers( solver: Solver, repo: Repository, package: ProjectPackage, create_package: PackageFactory, ) -> None: package.python_versions = ".".join([str(i) for i in sys.version_info[:3]]) package.add_dependency( Factory.create_dependency("A", {"version": "*", "extras": ["all"]}) ) create_package( "A", str(package.version), extras={ "download": ["download-package"], "install": ["install-package"], "all": ["a[download,install] ; python_version < '3.9'"], }, ) # Solving the dependency graph with solver.use_environment(MockEnv((3, 10, 0))): transaction = solver.solve() # Verifying the results check_solver_result( transaction, [ {"job": "install", "package": repo.package(name, package.version)} # FIXME: At the time of writing this test case, the markers from self-ref extras are not # correctly propagated into the dependency specs. For example, given this case, # the package "install-package" should have a final marker of # "extra == 'install' or extra == 'all' and python_version < '3.9'". # Once fixed, this should only install package "a". for name in ["download-package", "install-package", "a"] ], ) @pytest.mark.parametrize("enabled_extra", ["one", "two", None]) def test_solver_returns_extras_only_requested( solver: Solver, repo: Repository, package: ProjectPackage, enabled_extra: str | None, ) -> None: extras = [enabled_extra] if enabled_extra is not None else [] package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency( Factory.create_dependency("B", {"version": "*", "extras": extras}) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c10 = get_package("C", "1.0") package_c20 = get_package("C", "2.0") dep10 = get_dependency("C", "1.0", optional=True) dep10._in_extras = [canonicalize_name("one")] dep10.marker = parse_marker("extra == 'one'") dep20 = get_dependency("C", "2.0", optional=True) dep20._in_extras = [canonicalize_name("two")] dep20.marker = parse_marker("extra == 'two'") package_b.extras = { canonicalize_name("one"): [dep10], canonicalize_name("two"): [dep20], } package_b.add_dependency(dep10) package_b.add_dependency(dep20) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c10) repo.add_package(package_c20) transaction = solver.solve() expected = [ {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ] if enabled_extra is not None: expected.insert( 0, { "job": "install", "package": package_c10 if enabled_extra == "one" else package_c20, }, ) ops = check_solver_result( transaction, expected, ) assert ops[-1].package.marker.is_any() assert ops[0].package.marker.is_any() @pytest.mark.parametrize("enabled_extra", ["one", "two", None]) def test_solver_returns_extras_when_multiple_extras_use_same_dependency( solver: Solver, repo: Repository, package: ProjectPackage, enabled_extra: bool | None, ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") dep = get_dependency("C", "*", optional=True) dep._in_extras = [canonicalize_name("one"), canonicalize_name("two")] package_b.extras = { canonicalize_name("one"): [dep], canonicalize_name("two"): [dep], } package_b.add_dependency(dep) extras = [enabled_extra] if enabled_extra is not None else [] package_a.add_dependency( Factory.create_dependency("B", {"version": "*", "extras": extras}) ) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) transaction = solver.solve() expected = [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ] if enabled_extra is not None: expected.insert(0, {"job": "install", "package": package_c}) ops = check_solver_result( transaction, expected, ) assert ops[-1].package.marker.is_any() assert ops[0].package.marker.is_any() def test_solver_locks_all_extras_when_multiple_extras_require_same_dependency( solver: Solver, repo: Repository, package: ProjectPackage, ) -> None: """ - root depends on A[extra-b1] and C - C depends on A[extra-b2] - B is required by both extras -> the locked dependency A on B must have both extra markers """ package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") dep_b1 = get_dependency("B", "*", optional=True) dep_b1.marker = parse_marker("extra == 'extra-b1'") dep_b2 = get_dependency("B", "*", optional=True) dep_b2.marker = parse_marker("extra == 'extra-b2'") package_a.extras = { canonicalize_name("extra-b1"): [dep_b1], canonicalize_name("extra-b2"): [dep_b2], } package_a.add_dependency(dep_b1) package_a.add_dependency(dep_b2) package.add_dependency( get_dependency("A", {"version": "*", "extras": ["extra-b1"]}) ) package.add_dependency(get_dependency("C", "*")) package_c.add_dependency( get_dependency("A", {"version": "*", "extras": ["extra-b2"]}) ) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) transaction = solver.solve() expected = [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, {"job": "install", "package": package_c}, ] ops = check_solver_result(transaction, expected) locked_a_requires = ops[1].package.requires assert len(locked_a_requires) == 2 assert {str(r.marker) for r in locked_a_requires} == { 'extra == "extra-b1"', 'extra == "extra-b2"', } @pytest.mark.parametrize("enabled_extra", ["one", "two", None]) def test_solver_returns_extras_only_requested_nested( solver: Solver, repo: Repository, package: ProjectPackage, enabled_extra: str | None, ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c10 = get_package("C", "1.0") package_c20 = get_package("C", "2.0") dep10 = get_dependency("C", "1.0", optional=True) dep10._in_extras = [canonicalize_name("one")] dep10.marker = parse_marker("extra == 'one'") dep20 = get_dependency("C", "2.0", optional=True) dep20._in_extras = [canonicalize_name("two")] dep20.marker = parse_marker("extra == 'two'") package_b.extras = { canonicalize_name("one"): [dep10], canonicalize_name("two"): [dep20], } package_b.add_dependency(dep10) package_b.add_dependency(dep20) extras = [enabled_extra] if enabled_extra is not None else [] package_a.add_dependency( Factory.create_dependency("B", {"version": "*", "extras": extras}) ) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c10) repo.add_package(package_c20) transaction = solver.solve() expected = [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ] if enabled_extra is not None: expected.insert( 0, { "job": "install", "package": package_c10 if enabled_extra == "one" else package_c20, }, ) ops = check_solver_result(transaction, expected) assert ops[-1].package.marker.is_any() assert ops[0].package.marker.is_any() def test_solver_finds_extras_next_to_non_extras( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: # Root depends on A[foo] package.add_dependency( Factory.create_dependency("A", {"version": "*", "extras": ["foo"]}) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_d = get_package("D", "1.0") # A depends on B; A[foo] depends on B[bar]. package_a.add_dependency(Factory.create_dependency("B", "*")) package_a.add_dependency( Factory.create_dependency( "B", {"version": "*", "extras": ["bar"], "markers": "extra == 'foo'"} ) ) package_a.extras = {canonicalize_name("foo"): [get_dependency("B", "*")]} # B depends on C; B[bar] depends on D. package_b.add_dependency(Factory.create_dependency("C", "*")) package_b.add_dependency( Factory.create_dependency("D", {"version": "*", "markers": 'extra == "bar"'}) ) package_b.extras = {canonicalize_name("bar"): [get_dependency("D", "*")]} repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_c}, {"job": "install", "package": package_d}, {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) def test_solver_merge_extras_into_base_package_multiple_repos_fixes_5727( solver: Solver, repo: Repository, pool: RepositoryPool, package: ProjectPackage ) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "*", "source": "legacy"}) ) package.add_dependency(Factory.create_dependency("B", {"version": "*"})) package_a = get_package("A", "1.0") package_a.extras = {canonicalize_name("foo"): []} repo.add_package(package_a) package_b = Package("B", "1.0", source_type="legacy") package_b.add_dependency(package_a.with_features(["foo"]).to_dependency()) package_a = Package("A", "1.0", source_type="legacy", source_reference="legacy") package_a.extras = {canonicalize_name("foo"): []} repo = Repository("legacy") repo.add_package(package_a) repo.add_package(package_b) pool.add_repository(repo) transaction = solver.solve() ops = transaction.calculate_operations(synchronize=True) assert len(ops[0].package.requires) == 0, "a should not require itself" def test_solver_returns_extras_if_excluded_by_markers_without_extras( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "*", "extras": ["foo"]}) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") # mandatory dependency with marker dep = get_dependency("B", "^1.0") dep.marker = parse_marker("sys_platform != 'linux'") package_a.add_dependency(dep) # optional dependency with same constraint and no marker except for extra dep = get_dependency("B", "^1.0", optional=True) dep.marker = parse_marker("extra == 'foo'") package_a.extras = {canonicalize_name("foo"): [dep]} package_a.add_dependency(dep) repo.add_package(package_a) repo.add_package(package_b) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) assert ( str(ops[1].package.requires[0].marker) == 'sys_platform != "linux" or extra == "foo"' ) def test_solver_returns_prereleases_if_requested( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency( Factory.create_dependency("C", {"version": "*", "allow-prereleases": True}) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_c_dev = get_package("C", "1.1-beta.1") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_c_dev) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, {"job": "install", "package": package_c_dev}, ], ) def test_solver_does_not_return_prereleases_if_not_requested( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency(Factory.create_dependency("C", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_c_dev = get_package("C", "1.1-beta.1") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_c_dev) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, {"job": "install", "package": package_c}, ], ) def test_solver_sub_dependencies_with_requirements( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_d = get_package("D", "1.0") package_c.add_dependency( Factory.create_dependency("D", {"version": "^1.0", "python": "<4.0"}) ) package_a.add_dependency(Factory.create_dependency("C", "*")) package_b.add_dependency(Factory.create_dependency("D", "^1.0")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": package_d}, {"job": "install", "package": package_c}, {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ], ) op = ops[1] assert op.package.marker.is_any() def test_solver_sub_dependencies_with_requirements_complex( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "<5.0"}) ) package.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<5.0"}) ) package.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "python": "<4.0"}) ) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_d = get_package("D", "1.0") package_e = get_package("E", "1.0") package_f = get_package("F", "1.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<4.0"}) ) package_a.add_dependency( Factory.create_dependency("D", {"version": "^1.0", "python": "<4.0"}) ) package_b.add_dependency( Factory.create_dependency("E", {"version": "^1.0", "platform": "win32"}) ) package_b.add_dependency( Factory.create_dependency("F", {"version": "^1.0", "python": "<5.0"}) ) package_c.add_dependency( Factory.create_dependency("F", {"version": "^1.0", "python": "<4.0"}) ) package_d.add_dependency(Factory.create_dependency("F", "*")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) repo.add_package(package_e) repo.add_package(package_f) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_e}, {"job": "install", "package": package_f}, {"job": "install", "package": package_b}, {"job": "install", "package": package_d}, {"job": "install", "package": package_a}, {"job": "install", "package": package_c}, ], ) def test_solver_sub_dependencies_with_not_supported_python_version( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "^3.5") package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_b.python_versions = "<2.0" package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<2.0"}) ) repo.add_package(package_a) repo.add_package(package_b) transaction = solver.solve() check_solver_result(transaction, [{"job": "install", "package": package_a}]) def test_solver_sub_dependencies_with_not_supported_python_version_transitive( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "^3.4") package.add_dependency( Factory.create_dependency("httpx", {"version": "^0.17.1", "python": "^3.6"}) ) httpx = get_package("httpx", "0.17.1") httpx.python_versions = ">=3.6" httpcore = get_package("httpcore", "0.12.3") httpcore.python_versions = ">=3.6" sniffio_1_1_0 = get_package("sniffio", "1.1.0") sniffio_1_1_0.python_versions = ">=3.5" sniffio = get_package("sniffio", "1.2.0") sniffio.python_versions = ">=3.5" httpx.add_dependency( Factory.create_dependency("httpcore", {"version": ">=0.12.1,<0.13"}) ) httpx.add_dependency(Factory.create_dependency("sniffio", {"version": "*"})) httpcore.add_dependency(Factory.create_dependency("sniffio", {"version": "==1.*"})) repo.add_package(httpx) repo.add_package(httpcore) repo.add_package(sniffio) repo.add_package(sniffio_1_1_0) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": sniffio, "skipped": False}, {"job": "install", "package": httpcore, "skipped": False}, {"job": "install", "package": httpx, "skipped": False}, ], ) def test_solver_with_dependency_in_both_main_and_dev_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "^3.5") package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency( Factory.create_dependency( "A", {"version": "*", "extras": ["foo"]}, groups=["dev"] ) ) package_a = get_package("A", "1.0") package_a.extras = {canonicalize_name("foo"): [get_dependency("C")]} package_a.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "optional": True}) ) package_a.add_dependency(Factory.create_dependency("B", {"version": "^1.0"})) package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_c.add_dependency(Factory.create_dependency("D", "^1.0")) package_d = get_package("D", "1.0") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) transaction = solver.solve() _ = check_solver_result( transaction, [ {"job": "install", "package": package_d}, {"job": "install", "package": package_b}, {"job": "install", "package": package_c}, {"job": "install", "package": package_a}, ], ) def test_solver_with_dependency_in_both_main_and_dev_dependencies_with_one_more_dependent( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("E", "*")) package.add_dependency( Factory.create_dependency( "A", {"version": "*", "extras": ["foo"]}, groups=["dev"] ) ) package_a = get_package("A", "1.0") package_a.extras = {canonicalize_name("foo"): [get_dependency("C")]} package_a.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "optional": True}) ) package_a.add_dependency(Factory.create_dependency("B", {"version": "^1.0"})) package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_c.add_dependency(Factory.create_dependency("D", "^1.0")) package_d = get_package("D", "1.0") package_e = get_package("E", "1.0") package_e.add_dependency(Factory.create_dependency("A", "^1.0")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) repo.add_package(package_e) transaction = solver.solve() _ = check_solver_result( transaction, [ {"job": "install", "package": package_b}, {"job": "install", "package": package_d}, {"job": "install", "package": package_a}, {"job": "install", "package": package_c}, {"job": "install", "package": package_e}, ], ) def test_solver_with_dependency_and_prerelease_sub_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency(Factory.create_dependency("B", ">=1.0.0.dev2")) repo.add_package(package_a) repo.add_package(get_package("B", "0.9.0")) repo.add_package(get_package("B", "1.0.0.dev1")) repo.add_package(get_package("B", "1.0.0.dev2")) repo.add_package(get_package("B", "1.0.0.dev3")) package_b = get_package("B", "1.0.0.dev4") repo.add_package(package_b) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) def test_solver_with_dependency_and_prerelease_sub_dependencies_increasing_constraints( solver: Solver, repo: Repository, package: ProjectPackage, mocker: MockerFixture, ) -> None: """Regression test to ensure the solver eventually uses pre-release dependencies if the package is progressively constrained enough. This is different from test_solver_with_dependency_and_prerelease_sub_dependencies above because it also has a wildcard dependency on B at the root level. This causes the solver to first narrow B's candidate versions down to {0.9.0} at an early level, then eventually down to the empty set once A's dependencies are processed at a later level. Once the candidate version set is narrowed down to the empty set, the solver should re-evaluate available candidate versions from the source, but include pre-release versions this time as there are no other options. """ # Note: The order matters here; B must be added before A or the solver # evaluates A first and we don't encounter the issue. This is a bit # fragile, but the mock call assertions ensure this ordering is maintained. package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency(Factory.create_dependency("B", ">0.9.0")) repo.add_package(package_a) repo.add_package(get_package("B", "0.9.0")) package_b = get_package("B", "1.0.0.dev4") repo.add_package(package_b) search_for_spy = mocker.spy(solver._provider, "search_for") transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) # The assertions below aren't really the point of this test, but are just # being used to ensure the dependency resolution ordering remains the same. search_calls = [ call.args[0] for call in search_for_spy.mock_calls if call.args[0].name in ("a", "b") ] assert search_calls == [ Dependency("a", "*"), Dependency("b", "*"), Dependency("b", ">0.9.0"), ] def test_solver_circular_dependency( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0")) package_b = get_package("B", "1.0") package_b.add_dependency(Factory.create_dependency("A", "^1.0")) package_b.add_dependency(Factory.create_dependency("C", "^1.0")) package_c = get_package("C", "1.0") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) transaction = solver.solve() _ = check_solver_result( transaction, [ {"job": "install", "package": package_c}, {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) def test_solver_circular_dependency_chain( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0")) package_b = get_package("B", "1.0") package_b.add_dependency(Factory.create_dependency("C", "^1.0")) package_c = get_package("C", "1.0") package_c.add_dependency(Factory.create_dependency("D", "^1.0")) package_d = get_package("D", "1.0") package_d.add_dependency(Factory.create_dependency("B", "^1.0")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) transaction = solver.solve() _ = check_solver_result( transaction, [ {"job": "install", "package": package_d}, {"job": "install", "package": package_c}, {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) def test_solver_dense_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: # The root package depends on packages A0...An-1, # And package Ai depends on packages A0...Ai-1 # This graph is a transitive tournament packages = [] n = 22 for i in range(n): package_ai = get_package("a" + str(i), "1.0") repo.add_package(package_ai) packages.append(package_ai) package.add_dependency(Factory.create_dependency("a" + str(i), "^1.0")) for j in range(i): package_ai.add_dependency(Factory.create_dependency("a" + str(j), "^1.0")) transaction = solver.solve() check_solver_result( transaction, [{"job": "install", "package": packages[i]} for i in range(n)] ) def test_solver_duplicate_dependencies_same_constraint( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "2.7"}) ) package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": ">=3.4"}) ) package_b = get_package("B", "1.0") repo.add_package(package_a) repo.add_package(package_b) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) def test_solver_duplicate_dependencies_different_constraints( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<3.4"}) ) package_a.add_dependency( Factory.create_dependency("B", {"version": "^2.0", "python": ">=3.4"}) ) package_b10 = get_package("B", "1.0") package_b20 = get_package("B", "2.0") repo.add_package(package_a) repo.add_package(package_b10) repo.add_package(package_b20) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b10}, {"job": "install", "package": package_b20}, {"job": "install", "package": package_a}, ], ) def test_solver_duplicate_dependencies_different_constraints_same_requirements( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency(Factory.create_dependency("B", {"version": "^1.0"})) package_a.add_dependency(Factory.create_dependency("B", {"version": "^2.0"})) package_b10 = get_package("B", "1.0") package_b20 = get_package("B", "2.0") repo.add_package(package_a) repo.add_package(package_b10) repo.add_package(package_b20) with pytest.raises(IncompatibleConstraintsError) as e: solver.solve() expected = """\ Incompatible constraints in requirements of a (1.0): B (>=1.0,<2.0) B (>=2.0,<3.0)""" assert str(e.value) == expected def test_solver_duplicate_dependencies_different_constraints_merge_by_marker( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<3.4"}) ) package_a.add_dependency( Factory.create_dependency("B", {"version": "^2.0", "python": ">=3.4"}) ) package_a.add_dependency( Factory.create_dependency("B", {"version": "!=1.1", "python": "<3.4"}) ) package_b10 = get_package("B", "1.0") package_b11 = get_package("B", "1.1") package_b20 = get_package("B", "2.0") repo.add_package(package_a) repo.add_package(package_b10) repo.add_package(package_b11) repo.add_package(package_b20) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b10}, {"job": "install", "package": package_b20}, {"job": "install", "package": package_a}, ], ) @pytest.mark.parametrize("git_first", [False, True]) def test_solver_duplicate_dependencies_different_sources_direct_origin_preserved( solver: Solver, repo: Repository, package: ProjectPackage, git_first: bool ) -> None: pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) repo.add_package(get_package("cleo", "1.0.0")) repo.add_package(get_package("demo", "0.1.0")) dependency_pypi = Factory.create_dependency("demo", ">=0.1.0") dependency_git = Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git"}, groups=["dev"] ) if git_first: package.add_dependency(dependency_git) package.add_dependency(dependency_pypi) else: package.add_dependency(dependency_pypi) package.add_dependency(dependency_git) demo = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ) transaction = solver.solve() ops = check_solver_result( transaction, [{"job": "install", "package": pendulum}, {"job": "install", "package": demo}], ) op = ops[1] assert op.package.source_type == demo.source_type assert op.package.source_reference == DEFAULT_SOURCE_REF assert op.package.source_resolved_reference is not None assert demo.source_resolved_reference is not None assert op.package.source_resolved_reference.startswith( demo.source_resolved_reference ) complete_package = solver.provider.complete_package( DependencyPackage(package.to_dependency(), package) ) assert len(complete_package.package.all_requires) == 1 dep = complete_package.package.all_requires[0] assert isinstance(dep, VCSDependency) assert dep.constraint == demo.version assert (dep.name, dep.source_type, dep.source_url, dep.source_reference) == ( dependency_git.name, dependency_git.source_type, dependency_git.source_url, DEFAULT_SOURCE_REF, ) def test_solver_duplicate_dependencies_different_constraints_merge_no_markers( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "1.0")) package_a10 = get_package("A", "1.0") package_a10.add_dependency(Factory.create_dependency("C", {"version": "^1.0"})) package_a20 = get_package("A", "2.0") package_a20.add_dependency( Factory.create_dependency("C", {"version": "^2.0"}) # incompatible with B ) package_a20.add_dependency( Factory.create_dependency("C", {"version": "!=2.1", "python": "3.10"}) ) package_b = get_package("B", "1.0") package_b.add_dependency(Factory.create_dependency("C", {"version": "<2.0"})) package_c10 = get_package("C", "1.0") package_c20 = get_package("C", "2.0") package_c21 = get_package("C", "2.1") repo.add_package(package_a10) repo.add_package(package_a20) repo.add_package(package_b) repo.add_package(package_c10) repo.add_package(package_c20) repo.add_package(package_c21) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_c10}, {"job": "install", "package": package_a10}, # only a10, not a20 {"job": "install", "package": package_b}, ], ) def test_solver_duplicate_dependencies_different_constraints_conflict( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", ">=1.1")) package.add_dependency( Factory.create_dependency("A", {"version": "<1.1", "python": "3.10"}) ) repo.add_package(get_package("A", "1.0")) repo.add_package(get_package("A", "1.1")) repo.add_package(get_package("A", "1.2")) expectation = ( "Incompatible constraints in requirements of root (1.0):\n" "A (>=1.1)\n" 'A (<1.1) ; python_version == "3.10"' ) with pytest.raises(IncompatibleConstraintsError, match=re.escape(expectation)): solver.solve() def test_solver_duplicate_dependencies_different_constraints_discard_no_markers1( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: """ Initial dependencies: A (>=1.0) A (<1.2) ; python >= 3.10 A (<1.1) ; python < 3.10 Merged dependencies: A (>=1.0) ; A (>=1.0,<1.2) ; python >= 3.10 A (>=1.0,<1.1) ; python < 3.10 The dependency with an empty marker has to be ignored. """ package.add_dependency(Factory.create_dependency("A", ">=1.0")) package.add_dependency( Factory.create_dependency("A", {"version": "<1.2", "python": ">=3.10"}) ) package.add_dependency( Factory.create_dependency("A", {"version": "<1.1", "python": "<3.10"}) ) package.add_dependency(Factory.create_dependency("B", "*")) package_a10 = get_package("A", "1.0") package_a11 = get_package("A", "1.1") package_a12 = get_package("A", "1.2") package_b = get_package("B", "1.0") package_b.add_dependency(Factory.create_dependency("A", "*")) repo.add_package(package_a10) repo.add_package(package_a11) repo.add_package(package_a12) repo.add_package(package_b) transaction = solver.solve() check_solver_result( transaction, [ # only a10 and a11, not a12 {"job": "install", "package": package_a10}, {"job": "install", "package": package_a11}, {"job": "install", "package": package_b}, ], ) def test_solver_duplicate_dependencies_different_constraints_discard_no_markers2( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: """ Initial dependencies: A (>=1.0) A (<1.2) ; python == 3.10 Merged dependencies: A (>=1.0) ; python != 3.10 A (>=1.0,<1.2) ; python == 3.10 The first dependency has to be ignored because it is not compatible with the project's python constraint. """ set_package_python_versions(solver.provider, "~3.10") package.add_dependency(Factory.create_dependency("A", ">=1.0")) package.add_dependency( Factory.create_dependency("A", {"version": "<1.2", "python": "3.10"}) ) package.add_dependency(Factory.create_dependency("B", "*")) package_a10 = get_package("A", "1.0") package_a11 = get_package("A", "1.1") package_a12 = get_package("A", "1.2") package_b = get_package("B", "1.0") package_b.add_dependency(Factory.create_dependency("A", "*")) repo.add_package(package_a10) repo.add_package(package_a11) repo.add_package(package_a12) repo.add_package(package_b) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a11}, # only a11, not a12 {"job": "install", "package": package_b}, ], ) def test_solver_duplicate_dependencies_different_constraints_discard_no_markers3( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: """ Initial dependencies: A (>=1.0) A (<1.2) ; python == 3.10 Merged dependencies: A (>=1.0) ; python != 3.10 A (>=1.0,<1.2) ; python == 3.10 The first dependency has to be ignored because it is not compatible with the current environment. """ package.add_dependency(Factory.create_dependency("A", ">=1.0")) package.add_dependency( Factory.create_dependency("A", {"version": "<1.2", "python": "3.10"}) ) package.add_dependency(Factory.create_dependency("B", "*")) package_a10 = get_package("A", "1.0") package_a11 = get_package("A", "1.1") package_a12 = get_package("A", "1.2") package_b = get_package("B", "1.0") package_b.add_dependency(Factory.create_dependency("A", "*")) repo.add_package(package_a10) repo.add_package(package_a11) repo.add_package(package_a12) repo.add_package(package_b) with solver.use_environment(MockEnv((3, 10, 0))): transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a11}, # only a11, not a12 {"job": "install", "package": package_b}, ], ) def test_solver_duplicate_dependencies_ignore_overrides_with_empty_marker_intersection( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: """ Distinct requirements per marker: * Python 2.7: A (which requires B) and B * Python 3.6: same as Python 2.7 but with different versions * Python 3.7: only A * Python 3.8: only B """ package.add_dependency( Factory.create_dependency("A", {"version": "1.0", "python": "~2.7"}) ) package.add_dependency( Factory.create_dependency("A", {"version": "2.0", "python": "~3.6"}) ) package.add_dependency( Factory.create_dependency("A", {"version": "3.0", "python": "~3.7"}) ) package.add_dependency( Factory.create_dependency("B", {"version": "1.0", "python": "~2.7"}) ) package.add_dependency( Factory.create_dependency("B", {"version": "2.0", "python": "~3.6"}) ) package.add_dependency( Factory.create_dependency("B", {"version": "3.0", "python": "~3.8"}) ) package_a10 = get_package("A", "1.0") package_a10.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "~2.7"}) ) package_a20 = get_package("A", "2.0") package_a20.add_dependency( Factory.create_dependency("B", {"version": "^2.0", "python": "~3.6"}) ) package_a30 = get_package("A", "3.0") # no dep to B package_b10 = get_package("B", "1.0") package_b11 = get_package("B", "1.1") package_b20 = get_package("B", "2.0") package_b21 = get_package("B", "2.1") package_b30 = get_package("B", "3.0") repo.add_package(package_a10) repo.add_package(package_a20) repo.add_package(package_a30) repo.add_package(package_b10) repo.add_package(package_b11) repo.add_package(package_b20) repo.add_package(package_b21) repo.add_package(package_b30) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b10}, {"job": "install", "package": package_b20}, {"job": "install", "package": package_a10}, {"job": "install", "package": package_a20}, {"job": "install", "package": package_a30}, {"job": "install", "package": package_b30}, ], ) def test_solver_duplicate_dependencies_ignore_overrides_with_empty_marker_intersection2( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: """ Empty intersection between top level dependency and transitive dependency. """ package.add_dependency(Factory.create_dependency("A", {"version": "1.0"})) package.add_dependency( Factory.create_dependency("B", {"version": ">=2.0", "python": ">=3.7"}) ) package.add_dependency( Factory.create_dependency("B", {"version": "*", "python": "<3.7"}) ) package_a10 = get_package("A", "1.0") package_a10.add_dependency( Factory.create_dependency("B", {"version": ">=2.0", "python": ">=3.7"}) ) package_a10.add_dependency( Factory.create_dependency("B", {"version": "*", "python": "<3.7"}) ) package_b10 = get_package("B", "1.0") package_b10.python_versions = "<3.7" package_b20 = get_package("B", "2.0") package_b20.python_versions = ">=3.7" repo.add_package(package_a10) repo.add_package(package_b10) repo.add_package(package_b20) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b10}, {"job": "install", "package": package_b20}, {"job": "install", "package": package_a10}, ], ) def test_solver_duplicate_dependencies_sub_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") package_a.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "<3.4"}) ) package_a.add_dependency( Factory.create_dependency("B", {"version": "^2.0", "python": ">=3.4"}) ) package_b10 = get_package("B", "1.0") package_b20 = get_package("B", "2.0") package_b10.add_dependency(Factory.create_dependency("C", "1.2")) package_b20.add_dependency(Factory.create_dependency("C", "1.5")) package_c12 = get_package("C", "1.2") package_c15 = get_package("C", "1.5") repo.add_package(package_a) repo.add_package(package_b10) repo.add_package(package_b20) repo.add_package(package_c12) repo.add_package(package_c15) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_c12}, {"job": "install", "package": package_c15}, {"job": "install", "package": package_b10}, {"job": "install", "package": package_b20}, {"job": "install", "package": package_a}, ], ) def test_solver_duplicate_dependencies_with_overlapping_markers_simple( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(get_dependency("b", "1.0")) package_b = get_package("b", "1.0") dep_strings = [ "a (>=1.0)", "a (>=1.1) ; python_version >= '3.7'", "a (<2.0) ; python_version < '3.8'", "a (!=1.2) ; python_version == '3.7'", ] deps = [Dependency.create_from_pep_508(dep) for dep in dep_strings] for dep in deps: package_b.add_dependency(dep) package_a09 = get_package("a", "0.9") package_a10 = get_package("a", "1.0") package_a11 = get_package("a", "1.1") package_a12 = get_package("a", "1.2") package_a20 = get_package("a", "2.0") package_a11.python_versions = ">=3.7" package_a12.python_versions = ">=3.7" package_a20.python_versions = ">=3.7" repo.add_package(package_a09) repo.add_package(package_a10) repo.add_package(package_a11) repo.add_package(package_a12) repo.add_package(package_a20) repo.add_package(package_b) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": package_a10}, {"job": "install", "package": package_a11}, {"job": "install", "package": package_a20}, {"job": "install", "package": package_b}, ], ) package_b_requires = {dep.to_pep_508() for dep in ops[-1].package.requires} assert package_b_requires == { 'a (>=1.0,<2.0) ; python_version < "3.7"', 'a (>=1.1,!=1.2,<2.0) ; python_version == "3.7"', 'a (>=1.1) ; python_version >= "3.8"', } def test_solver_duplicate_dependencies_with_overlapping_markers_complex( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: """ Dependencies with overlapping markers from https://pypi.org/project/opencv-python/4.6.0.66/ """ package.add_dependency(get_dependency("opencv", "4.6.0.66")) opencv_package = get_package("opencv", "4.6.0.66") dep_strings = [ "numpy (>=1.13.3) ; python_version < '3.7'", "numpy (>=1.21.2) ; python_version >= '3.10'", ( "numpy (>=1.21.2) ; python_version >= '3.6' " "and platform_system == 'Darwin' and platform_machine == 'arm64'" ), ( "numpy (>=1.19.3) ; python_version >= '3.6' " "and platform_system == 'Linux' and platform_machine == 'aarch64'" ), "numpy (>=1.14.5) ; python_version >= '3.7'", "numpy (>=1.17.3) ; python_version >= '3.8'", "numpy (>=1.19.3) ; python_version >= '3.9'", ] deps = [Dependency.create_from_pep_508(dep) for dep in dep_strings] for dep in deps: opencv_package.add_dependency(dep) for version in {"1.13.3", "1.21.2", "1.19.3", "1.14.5", "1.17.3"}: repo.add_package(get_package("numpy", version)) repo.add_package(opencv_package) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": get_package("numpy", "1.21.2")}, {"job": "install", "package": opencv_package}, ], ) opencv_requires = {dep.to_pep_508() for dep in ops[-1].package.requires} # before https://github.com/python-poetry/poetry-core/pull/851 expected1 = { ( "numpy (>=1.21.2) ;" ' python_version >= "3.6" and platform_system == "Darwin"' ' and platform_machine == "arm64" or python_version >= "3.10"' ), ( 'numpy (>=1.19.3) ; platform_system == "Linux"' ' and platform_machine == "aarch64" and python_version < "3.10"' ' and python_version >= "3.6" or python_version == "3.9"' ' and platform_system != "Darwin" or python_version == "3.9"' ' and platform_machine != "arm64"' ), ( 'numpy (>=1.17.3) ; python_version == "3.8"' ' and (platform_system != "Darwin" or platform_machine != "arm64")' ' and (platform_system != "Linux" or platform_machine != "aarch64")' ), ( 'numpy (>=1.14.5) ; python_version == "3.7"' ' and (platform_system != "Darwin" or platform_machine != "arm64")' ' and (platform_system != "Linux" or platform_machine != "aarch64")' ), ( 'numpy (>=1.13.3) ; python_version < "3.7"' ' and (python_version < "3.6" or platform_system != "Darwin"' ' or platform_machine != "arm64") and (python_version < "3.6"' ' or platform_system != "Linux" or platform_machine != "aarch64")' ), } # after https://github.com/python-poetry/poetry-core/pull/851 expected2 = { ( "numpy (>=1.21.2) ;" ' platform_system == "Darwin" and platform_machine == "arm64"' ' and python_version >= "3.6" or python_version >= "3.10"' ), ( 'numpy (>=1.19.3) ; python_version >= "3.6" and python_version < "3.10"' ' and platform_system == "Linux" and platform_machine == "aarch64"' ' or python_version == "3.9" and platform_machine != "arm64"' ' or python_version == "3.9" and platform_system != "Darwin"' ), ( 'numpy (>=1.17.3) ; python_version == "3.8"' ' and (platform_system != "Darwin" or platform_machine != "arm64")' ' and (platform_system != "Linux" or platform_machine != "aarch64")' ), ( 'numpy (>=1.14.5) ; python_version == "3.7"' ' and (platform_system != "Darwin" or platform_machine != "arm64")' ' and (platform_system != "Linux" or platform_machine != "aarch64")' ), ( 'numpy (>=1.13.3) ; python_version < "3.7"' ' and (python_version < "3.6" or platform_system != "Darwin"' ' or platform_machine != "arm64") and (python_version < "3.6"' ' or platform_system != "Linux" or platform_machine != "aarch64")' ), } assert opencv_requires in (expected1, expected2) def test_duplicate_path_dependencies( solver: Solver, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: set_package_python_versions(solver.provider, "^3.7") project_dir = shutil.copytree( fixture_dir("with_conditional_path_deps"), tmp_path / "project" ) path1 = (project_dir / "demo_one").as_posix() demo1 = Package("demo", "1.2.3", source_type="directory", source_url=path1) package.add_dependency( Factory.create_dependency( "demo", {"path": path1, "markers": "sys_platform == 'linux'"} ) ) path2 = (project_dir / "demo_two").as_posix() demo2 = Package("demo", "1.2.3", source_type="directory", source_url=path2) package.add_dependency( Factory.create_dependency( "demo", {"path": path2, "markers": "sys_platform == 'win32'"} ) ) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": demo1}, {"job": "install", "package": demo2}, ], ) def test_duplicate_path_dependencies_same_path( solver: Solver, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: set_package_python_versions(solver.provider, "^3.7") project_dir = shutil.copytree( fixture_dir("with_conditional_path_deps"), tmp_path / "project" ) path1 = (project_dir / "demo_one").as_posix() demo1 = Package("demo", "1.2.3", source_type="directory", source_url=path1) package.add_dependency( Factory.create_dependency( "demo", {"path": path1, "markers": "sys_platform == 'linux'"} ) ) package.add_dependency( Factory.create_dependency( "demo", {"path": path1, "markers": "sys_platform == 'win32'"} ) ) transaction = solver.solve() check_solver_result(transaction, [{"job": "install", "package": demo1}]) def test_solver_fails_if_dependency_name_does_not_match_package( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency( Factory.create_dependency( "my-demo", {"git": "https://github.com/demo/demo.git"} ) ) with pytest.raises(RuntimeError): solver.solve() def test_solver_does_not_get_stuck_in_recursion_on_circular_dependency( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package_a = get_package("A", "1.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0")) package_b = get_package("B", "1.0") package_b.add_dependency(Factory.create_dependency("C", "^1.0")) package_c = get_package("C", "1.0") package_c.add_dependency(Factory.create_dependency("B", "^1.0")) repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) package.add_dependency(Factory.create_dependency("A", "^1.0")) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_c}, {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) def test_solver_can_resolve_git_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) transaction = solver.solve() demo = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ) ops = check_solver_result( transaction, [{"job": "install", "package": pendulum}, {"job": "install", "package": demo}], ) op = ops[1] assert op.package.source_type == "git" assert op.package.source_reference == DEFAULT_SOURCE_REF assert op.package.source_resolved_reference is not None assert op.package.source_resolved_reference.startswith("9cf87a2") def test_solver_can_resolve_git_dependencies_with_extras( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) package.add_dependency( Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git", "extras": ["foo"]} ) ) transaction = solver.solve() demo = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ) check_solver_result( transaction, [ {"job": "install", "package": cleo}, {"job": "install", "package": pendulum}, {"job": "install", "package": demo}, ], ) @pytest.mark.parametrize( "ref", [{"branch": "a-branch"}, {"tag": "a-tag"}, {"rev": "9cf8"}], ids=["branch", "tag", "rev"], ) def test_solver_can_resolve_git_dependencies_with_ref( solver: Solver, repo: Repository, package: ProjectPackage, ref: dict[str, str] ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) demo = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=ref[next(iter(ref.keys()))], source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ) assert demo.source_type is not None assert demo.source_url is not None git_config = {demo.source_type: demo.source_url} git_config.update(ref) package.add_dependency(Factory.create_dependency("demo", git_config)) transaction = solver.solve() ops = check_solver_result( transaction, [{"job": "install", "package": pendulum}, {"job": "install", "package": demo}], ) op = ops[1] assert op.package.source_type == "git" assert op.package.source_reference == ref[next(iter(ref.keys()))] assert op.package.source_resolved_reference is not None assert op.package.source_resolved_reference.startswith("9cf87a2") def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requirement_is_compatible( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.6"}) ) package_a = get_package("A", "1.0.0") package_a.python_versions = ">=3.6" repo.add_package(package_a) transaction = solver.solve() check_solver_result(transaction, [{"job": "install", "package": package_a}]) def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requirement_is_compatible_multiple( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.6"}) ) package.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "^3.5.3"}) ) package_a = get_package("A", "1.0.0") package_a.python_versions = ">=3.6" package_a.add_dependency(Factory.create_dependency("B", "^1.0")) package_b = get_package("B", "1.0.0") package_b.python_versions = ">=3.5.3" repo.add_package(package_a) repo.add_package(package_b) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b}, {"job": "install", "package": package_a}, ], ) def test_solver_triggers_conflict_for_dependency_python_not_fully_compatible_with_package_python( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.5"}) ) package_a = get_package("A", "1.0.0") package_a.python_versions = ">=3.6" repo.add_package(package_a) with pytest.raises(SolverProblemError): solver.solve() def test_solver_finds_compatible_package_for_dependency_python_not_fully_compatible_with_package_python( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.5"}) ) package_a101 = get_package("A", "1.0.1") package_a101.python_versions = ">=3.6" package_a100 = get_package("A", "1.0.0") package_a100.python_versions = ">=3.5" repo.add_package(package_a100) repo.add_package(package_a101) transaction = solver.solve() check_solver_result(transaction, [{"job": "install", "package": package_a100}]) def test_solver_does_not_trigger_new_resolution_on_duplicate_dependencies_if_only_extras( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: dep1 = Dependency.create_from_pep_508('B (>=1.0); extra == "foo"') dep1.activate() dep2 = Dependency.create_from_pep_508('B (>=2.0); extra == "bar"') dep2.activate() package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "extras": ["foo", "bar"]}) ) package_a = get_package("A", "1.0.0") package_a.extras = { canonicalize_name("foo"): [dep1], canonicalize_name("bar"): [dep2], } package_a.add_dependency(dep1) package_a.add_dependency(dep2) package_b2 = get_package("B", "2.0.0") package_b1 = get_package("B", "1.0.0") repo.add_package(package_a) repo.add_package(package_b1) repo.add_package(package_b2) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": package_b2}, {"job": "install", "package": package_a}, ], ) assert str(ops[0].package.marker) == "" assert str(ops[1].package.marker) == "" def test_solver_does_not_raise_conflict_for_locked_conditional_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") dependency_a = Factory.create_dependency("A", {"version": "^1.0", "python": "^3.6"}) package.add_dependency(dependency_a) package.add_dependency(Factory.create_dependency("B", "^1.0")) package_a = get_package("A", "1.0.0") package_a.python_versions = ">=3.6" package_a.marker = parse_marker( 'python_version >= "3.6" and python_version < "4.0"' ) package_b = get_package("B", "1.0.0") repo.add_package(package_a) repo.add_package(package_b) dep_package_a = DependencyPackage(dependency_a, package_a) solver.provider._locked = {canonicalize_name("A"): [dep_package_a]} transaction = solver.solve(use_latest=[package_b.name]) check_solver_result( transaction, [ {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ], ) def test_solver_returns_extras_if_requested_in_dependencies_and_not_in_root_package( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency(Factory.create_dependency("C", "*")) package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") package_d = get_package("D", "1.0") package_b.add_dependency( Factory.create_dependency("C", {"version": "^1.0", "extras": ["foo"]}) ) package_c.add_dependency( Factory.create_dependency("D", {"version": "^1.0", "optional": True}) ) package_c.extras = { canonicalize_name("foo"): [Factory.create_dependency("D", "^1.0")] } repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) repo.add_package(package_d) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_d}, {"job": "install", "package": package_c}, {"job": "install", "package": package_a}, {"job": "install", "package": package_b}, ], ) def test_solver_should_not_resolve_prerelease_version_if_not_requested( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("A", "~1.8.0")) package.add_dependency(Factory.create_dependency("B", "^0.5.0")) package_a185 = get_package("A", "1.8.5") package_a19b1 = get_package("A", "1.9b1") package_b = get_package("B", "0.5.0") package_b.add_dependency(Factory.create_dependency("A", ">=1.9b1")) repo.add_package(package_a185) repo.add_package(package_a19b1) repo.add_package(package_b) with pytest.raises(SolverProblemError): solver.solve() def test_solver_ignores_dependencies_with_incompatible_python_full_version_marker( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "^3.6") package.add_dependency(Factory.create_dependency("A", "^1.0")) package.add_dependency(Factory.create_dependency("B", "^2.0")) package_a = get_package("A", "1.0.0") package_a.add_dependency( Dependency.create_from_pep_508( 'B (<2.0); platform_python_implementation == "PyPy" and python_full_version' ' < "2.7.9"' ) ) package_b200 = get_package("B", "2.0.0") package_b100 = get_package("B", "1.0.0") repo.add_package(package_a) repo.add_package(package_b100) repo.add_package(package_b200) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a}, {"job": "install", "package": package_b200}, ], ) def test_solver_git_dependencies_update( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) demo_installed = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference="123456", ) demo = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ) package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) solver = Solver(package, pool, [demo_installed], [], io) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": pendulum}, {"job": "update", "from": demo_installed, "to": demo}, ], ) op = ops[1] assert op.job_type == "update" assert isinstance(op, Update) assert op.package.source_type == "git" assert op.package.source_reference == DEFAULT_SOURCE_REF assert op.package.source_resolved_reference is not None assert op.package.source_resolved_reference.startswith("9cf87a2") assert op.initial_package.source_resolved_reference == "123456" def test_solver_git_dependencies_update_skipped( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) demo = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference="master", source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ) package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) solver = Solver(package, pool, [demo], [], io) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": pendulum}, {"job": "install", "package": demo, "skipped": True}, ], ) def test_solver_git_dependencies_short_hash_update_skipped( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) demo = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=MOCK_DEFAULT_GIT_REVISION, source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ) package.add_dependency( Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git", "rev": "9cf87a2"} ) ) solver = Solver(package, pool, [demo], [], io) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": pendulum}, { "job": "install", "package": Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=MOCK_DEFAULT_GIT_REVISION, source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ), "skipped": True, }, ], ) def test_solver_can_resolve_directory_dependencies( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) project_dir = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project", ) path = project_dir.as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) transaction = solver.solve() demo = Package("demo", "0.1.2", source_type="directory", source_url=path) ops = check_solver_result( transaction, [{"job": "install", "package": pendulum}, {"job": "install", "package": demo}], ) op = ops[1] assert op.package.name == "demo" assert op.package.version.text == "0.1.2" assert op.package.source_type == "directory" assert op.package.source_url == path def test_solver_can_resolve_directory_dependencies_nested_editable( repo: Repository, pool: RepositoryPool, io: NullIO, fixture_dir: FixtureDirGetter, ) -> None: base = fixture_dir("project_with_nested_local") poetry = Factory().create_poetry(cwd=base) package = poetry.package solver = Solver(package, pool, [], [], io) transaction = solver.solve() ops = check_solver_result( transaction, [ { "job": "install", "package": Package( "quix", "1.2.3", source_type="directory", source_url=(base / "quix").as_posix(), ), "skipped": False, }, { "job": "install", "package": Package( "bar", "1.2.3", source_type="directory", source_url=(base / "bar").as_posix(), ), "skipped": False, }, { "job": "install", "package": Package( "foo", "1.2.3", source_type="directory", source_url=(base / "foo").as_posix(), ), "skipped": False, }, ], ) for op in ops: assert op.package.source_type == "directory" assert op.package.develop is True def test_solver_can_resolve_directory_dependencies_with_extras( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) project_dir = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project", ) path = project_dir.as_posix() package.add_dependency( Factory.create_dependency("demo", {"path": path, "extras": ["foo"]}) ) transaction = solver.solve() demo = Package("demo", "0.1.2", source_type="directory", source_url=path) ops = check_solver_result( transaction, [ {"job": "install", "package": cleo}, {"job": "install", "package": pendulum}, {"job": "install", "package": demo}, ], ) op = ops[2] assert op.package.name == "demo" assert op.package.version.text == "0.1.2" assert op.package.source_type == "directory" assert op.package.source_url == path def test_solver_can_resolve_sdist_dependencies( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) project_dir = tmp_path / "project" project_dir.mkdir() path = shutil.copy(fixture_dir("distributions") / "demo-0.1.0.tar.gz", project_dir) path = Path(path).as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) transaction = solver.solve() demo = Package("demo", "0.1.0", source_type="file", source_url=path) ops = check_solver_result( transaction, [{"job": "install", "package": pendulum}, {"job": "install", "package": demo}], ) op = ops[1] assert op.package.name == "demo" assert op.package.version.text == "0.1.0" assert op.package.source_type == "file" assert op.package.source_url == path def test_solver_can_resolve_sdist_dependencies_with_extras( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) project_dir = tmp_path / "project" project_dir.mkdir() path = shutil.copy(fixture_dir("distributions") / "demo-0.1.0.tar.gz", project_dir) path = Path(path).as_posix() package.add_dependency( Factory.create_dependency("demo", {"path": path, "extras": ["foo"]}) ) transaction = solver.solve() demo = Package("demo", "0.1.0", source_type="file", source_url=path) ops = check_solver_result( transaction, [ {"job": "install", "package": cleo}, {"job": "install", "package": pendulum}, {"job": "install", "package": demo}, ], ) op = ops[2] assert op.package.name == "demo" assert op.package.version.text == "0.1.0" assert op.package.source_type == "file" assert op.package.source_url == path def test_solver_can_resolve_wheel_dependencies( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) project_dir = tmp_path / "project" project_dir.mkdir() path = shutil.copy( fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl", project_dir ) path = Path(path).as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) transaction = solver.solve() demo = Package("demo", "0.1.0", source_type="file", source_url=path) ops = check_solver_result( transaction, [{"job": "install", "package": pendulum}, {"job": "install", "package": demo}], ) op = ops[1] assert op.package.name == "demo" assert op.package.version.text == "0.1.0" assert op.package.source_type == "file" assert op.package.source_url == path def test_solver_can_resolve_wheel_dependencies_with_extras( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) project_dir = tmp_path / "project" project_dir.mkdir() path = shutil.copy( fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl", project_dir ) path = Path(path).as_posix() package.add_dependency( Factory.create_dependency("demo", {"path": path, "extras": ["foo"]}) ) transaction = solver.solve() demo = Package("demo", "0.1.0", source_type="file", source_url=path) ops = check_solver_result( transaction, [ {"job": "install", "package": cleo}, {"job": "install", "package": pendulum}, {"job": "install", "package": demo}, ], ) op = ops[2] assert op.package.name == "demo" assert op.package.version.text == "0.1.0" assert op.package.source_type == "file" assert op.package.source_url == path def test_solver_can_solve_with_legacy_repository_using_proper_dists( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository ) -> None: repo = legacy_repository pool = RepositoryPool([repo]) solver = Solver(package, pool, [], [], io) package.add_dependency(Factory.create_dependency("isort", "4.3.4")) transaction = solver.solve() ops = check_solver_result( transaction, [ { "job": "install", "package": Package( "futures", "3.2.0", source_type="legacy", source_url=repo.url, source_reference=repo.name, ), }, { "job": "install", "package": Package( "isort", "4.3.4", source_type="legacy", source_url=repo.url, source_reference=repo.name, ), }, ], ) futures = ops[0].package assert futures.python_versions == ">=2.6, <3" def test_solver_can_solve_with_legacy_repository_using_proper_python_compatible_dists( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, ) -> None: package.python_versions = "^3.7" repo = legacy_repository pool = RepositoryPool([repo]) solver = Solver(package, pool, [], [], io) package.add_dependency(Factory.create_dependency("isort", "4.3.4")) transaction = solver.solve() check_solver_result( transaction, [ { "job": "install", "package": Package( "isort", "4.3.4", source_type="legacy", source_url=repo.url, source_reference=repo.name, ), } ], ) def test_solver_skips_invalid_versions( package: ProjectPackage, io: NullIO, pypi_repository: PyPiRepository ) -> None: package.python_versions = "^3.9" pool = RepositoryPool([pypi_repository]) solver = Solver(package, pool, [], [], io) package.add_dependency(Factory.create_dependency("six-unknown-version", "^1.11")) transaction = solver.solve() check_solver_result( transaction, [{"job": "install", "package": get_package("six-unknown-version", "1.11.0")}], ) def test_multiple_constraints_on_root( package: ProjectPackage, solver: Solver, repo: Repository ) -> None: package.add_dependency( Factory.create_dependency("foo", {"version": "^1.0", "python": "^2.7"}) ) package.add_dependency( Factory.create_dependency("foo", {"version": "^2.0", "python": "^3.7"}) ) foo15 = get_package("foo", "1.5.0") foo25 = get_package("foo", "2.5.0") repo.add_package(foo15) repo.add_package(foo25) transaction = solver.solve() check_solver_result( transaction, [{"job": "install", "package": foo15}, {"job": "install", "package": foo25}], ) def test_solver_chooses_most_recent_version_amongst_repositories( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, ) -> None: package.python_versions = "^3.7" package.add_dependency(Factory.create_dependency("tomlkit", {"version": "^0.5"})) pool = RepositoryPool([legacy_repository, pypi_repository]) solver = Solver(package, pool, [], [], io) transaction = solver.solve() ops = check_solver_result( transaction, [{"job": "install", "package": get_package("tomlkit", "0.5.3")}] ) assert ops[0].package.source_type is None assert ops[0].package.source_url is None def test_solver_chooses_from_correct_repository_if_forced( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, ) -> None: package.python_versions = "^3.7" package.add_dependency( Factory.create_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) ) pool = RepositoryPool([legacy_repository, pypi_repository]) solver = Solver(package, pool, [], [], io) transaction = solver.solve() ops = check_solver_result( transaction, [ { "job": "install", "package": Package( "tomlkit", "0.5.2", source_type="legacy", source_url=legacy_repository.url, source_reference=legacy_repository.name, ), } ], ) assert ops[0].package.source_url == legacy_repository.url @pytest.mark.parametrize("project_dependencies", [True, False]) def test_solver_chooses_from_correct_repository_if_forced_and_transitive_dependency( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, project_dependencies: bool, ) -> None: package.python_versions = "^3.7" if project_dependencies: main_group = DependencyGroup(MAIN_GROUP) package.add_dependency_group(main_group) main_group.add_dependency(Factory.create_dependency("foo", "^1.0")) main_group.add_dependency(Factory.create_dependency("tomlkit", "^0.5")) main_group.add_poetry_dependency( Factory.create_dependency("tomlkit", {"source": "legacy"}) ) else: package.add_dependency(Factory.create_dependency("foo", "^1.0")) package.add_dependency( Factory.create_dependency( "tomlkit", {"version": "^0.5", "source": "legacy"} ) ) repo = Repository("repo") foo = get_package("foo", "1.0.0") foo.add_dependency(Factory.create_dependency("tomlkit", "^0.5.0")) repo.add_package(foo) pool = RepositoryPool([legacy_repository, repo, pypi_repository]) solver = Solver(package, pool, [], [], io) transaction = solver.solve() ops = check_solver_result( transaction, [ { "job": "install", "package": Package( "tomlkit", "0.5.2", source_type="legacy", source_url=legacy_repository.url, source_reference="legacy", ), }, {"job": "install", "package": foo}, ], ) assert ops[0].package.source_url == legacy_repository.url assert ops[1].package.source_type is None assert ops[1].package.source_url is None def test_solver_does_not_choose_from_supplemental_repository_by_default( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, ) -> None: package.python_versions = "^3.7" package.add_dependency(Factory.create_dependency("clikit", {"version": "^0.2.0"})) pool = RepositoryPool() pool.add_repository(pypi_repository, priority=Priority.SUPPLEMENTAL) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) transaction = solver.solve() ops = check_solver_result( transaction, [ { "job": "install", "package": Package( "pastel", "0.1.0", source_type="legacy", source_url=legacy_repository.url, source_reference="legacy", ), }, {"job": "install", "package": get_package("pylev", "1.3.0")}, { "job": "install", "package": Package( "clikit", "0.2.4", source_type="legacy", source_url=legacy_repository.url, source_reference="legacy", ), }, ], ) assert ops[0].package.source_url == legacy_repository.url assert ops[1].package.source_type is None assert ops[1].package.source_url is None assert ops[2].package.source_url == legacy_repository.url def test_solver_chooses_from_supplemental_if_explicit( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, ) -> None: package.python_versions = "^3.7" package.add_dependency( Factory.create_dependency("clikit", {"version": "^0.2.0", "source": "PyPI"}) ) pool = RepositoryPool() pool.add_repository(pypi_repository, priority=Priority.SUPPLEMENTAL) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) transaction = solver.solve() ops = check_solver_result( transaction, [ { "job": "install", "package": Package( "pastel", "0.1.0", source_type="legacy", source_url=legacy_repository.url, source_reference="legacy", ), }, {"job": "install", "package": get_package("pylev", "1.3.0")}, {"job": "install", "package": get_package("clikit", "0.2.4")}, ], ) assert ops[0].package.source_url == legacy_repository.url assert ops[1].package.source_type is None assert ops[1].package.source_url is None assert ops[2].package.source_type is None assert ops[2].package.source_url is None def test_solver_does_not_choose_from_explicit_repository( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, ) -> None: package.python_versions = "^3.7" package.add_dependency(Factory.create_dependency("attrs", {"version": "^17.4.0"})) pool = RepositoryPool() pool.add_repository(pypi_repository, priority=Priority.EXPLICIT) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) with pytest.raises(SolverProblemError): solver.solve() def test_solver_chooses_direct_dependency_from_explicit_if_explicit( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, ) -> None: package.python_versions = "^3.7" package.add_dependency( Factory.create_dependency("pylev", {"version": "^1.2.0", "source": "PyPI"}) ) pool = RepositoryPool() pool.add_repository(pypi_repository, priority=Priority.EXPLICIT) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": get_package("pylev", "1.3.0")}, ], ) assert ops[0].package.source_type is None assert ops[0].package.source_url is None def test_solver_ignores_explicit_repo_for_transitive_dependencies( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, ) -> None: # clikit depends on pylev, which is in pypi_repository (explicit) but not in # legacy_repository package.python_versions = "^3.7" package.add_dependency( Factory.create_dependency("clikit", {"version": "^0.2.0", "source": "PyPI"}) ) pool = RepositoryPool() pool.add_repository(pypi_repository, priority=Priority.EXPLICIT) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) with pytest.raises(SolverProblemError): solver.solve() @pytest.mark.parametrize( ("lib_versions", "other_versions"), [ # number of versions influences which dependency is resolved first (["1.0", "2.0"], ["1.0", "1.1", "2.0"]), # more other than lib (["1.0", "1.1", "2.0"], ["1.0", "2.0"]), # more lib than other ], ) def test_direct_dependency_with_extras_from_explicit_and_transitive_dependency( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, lib_versions: list[str], other_versions: list[str], ) -> None: """ The root package depends on "lib[extra]" and "other", both with an explicit source. "other" depends on "lib" (without an extra and of course without an explicit source because explicit sources can only be defined in the root package). If "other" is resolved before "lib[extra]", the solver must not try to fetch "lib" from the default source but from the explicit source defined for "lib[extra]". """ package.add_dependency( Factory.create_dependency( "lib", {"version": ">=1.0", "extras": ["extra"], "source": "explicit"} ) ) package.add_dependency( Factory.create_dependency("other", {"version": ">=1.0", "source": "explicit"}) ) explicit_repo = Repository("explicit") pool.add_repository(explicit_repo, priority=Priority.EXPLICIT) package_extra = get_package("extra", "1.0") repo.add_package(package_extra) # extra only in default repo for version in lib_versions: package_lib = Package( "lib", version, source_type="legacy", source_reference="explicit" ) dep_extra = get_dependency("extra", ">=1.0") package_lib.add_dependency( Factory.create_dependency("extra", {"version": ">=1.0", "optional": True}) ) package_lib.extras = {canonicalize_name("extra"): [dep_extra]} explicit_repo.add_package(package_lib) # lib only in explicit repo for version in other_versions: package_other = Package( "other", version, source_type="legacy", source_reference="explicit" ) package_other.add_dependency(Factory.create_dependency("lib", ">=1.0")) explicit_repo.add_package(package_other) # other only in explicit repo solver = Solver(package, pool, [], [], io) transaction = solver.solve() expected_lib = Package( "lib", "2.0", source_type="legacy", source_reference="explicit" ) expected_other = Package( "other", "2.0", source_type="legacy", source_reference="explicit" ) check_solver_result( transaction, [ {"job": "install", "package": get_package("extra", "1.0")}, {"job": "install", "package": expected_lib}, {"job": "install", "package": expected_other}, ], ) @pytest.mark.parametrize( ("lib_versions", "other_versions"), [ # number of versions influences which dependency is resolved first (["1.0", "2.0"], ["1.0", "1.1", "2.0"]), # more other than lib (["1.0", "1.1", "2.0"], ["1.0", "2.0"]), # more lib than other ], ) def test_direct_dependency_with_extras_from_explicit_and_transitive_dependency2( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, lib_versions: list[str], other_versions: list[str], ) -> None: """ The root package depends on "lib[extra]" and "other", both with an explicit source. "other" depends on "lib[other-extra]" (with another extra and of course without an explicit source because explicit sources can only be defined in the root package). The solver must not try to fetch "lib[other-extra]" from the default source but from the explicit source defined for "lib[extra]". """ package.add_dependency( Factory.create_dependency( "lib", {"version": ">=1.0", "extras": ["extra"], "source": "explicit"} ) ) package.add_dependency( Factory.create_dependency("other", {"version": ">=1.0", "source": "explicit"}) ) explicit_repo = Repository("explicit") pool.add_repository(explicit_repo, priority=Priority.EXPLICIT) package_extra = get_package("extra", "1.0") repo.add_package(package_extra) # extra only in default repo package_other_extra = get_package("other-extra", "1.0") repo.add_package(package_other_extra) # extra only in default repo for version in lib_versions: package_lib = Package( "lib", version, source_type="legacy", source_reference="explicit" ) dep_extra = get_dependency("extra", ">=1.0") package_lib.add_dependency( Factory.create_dependency("extra", {"version": ">=1.0", "optional": True}) ) dep_other_extra = get_dependency("other-extra", ">=1.0") package_lib.add_dependency( Factory.create_dependency( "other-extra", {"version": ">=1.0", "optional": True} ) ) package_lib.extras = { canonicalize_name("extra"): [dep_extra], canonicalize_name("other-extra"): [dep_other_extra], } explicit_repo.add_package(package_lib) # lib only in explicit repo for version in other_versions: package_other = Package( "other", version, source_type="legacy", source_reference="explicit" ) package_other.add_dependency( Factory.create_dependency( "lib", {"version": ">=1.0", "extras": ["other-extra"]} ) ) explicit_repo.add_package(package_other) # other only in explicit repo solver = Solver(package, pool, [], [], io) transaction = solver.solve() expected_lib = Package( "lib", "2.0", source_type="legacy", source_reference="explicit" ) expected_other = Package( "other", "2.0", source_type="legacy", source_reference="explicit" ) check_solver_result( transaction, [ {"job": "install", "package": get_package("other-extra", "1.0")}, {"job": "install", "package": get_package("extra", "1.0")}, {"job": "install", "package": expected_lib}, {"job": "install", "package": expected_other}, ], ) @pytest.mark.parametrize("locked", [False, True]) def test_multiple_constraints_explicit_source_transitive_locked_use_latest( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, locked: bool, ) -> None: """ The root package depends on * lib[extra] == 1.0; sys_platform != "linux" with source=explicit1 * lib[extra] == 2.0; sys_platform == "linux" with source=explicit2 * other >= 1.0 "other" depends on "lib" (without an extra and of course without an explicit source because explicit sources can only be defined in the root package). If only "other" is in use_latest (equivalent to "poetry update other"), the transitive dependency of "other" on "lib" is resolved before the direct dependency on "lib[extra]" (if packages have been locked before). We still have to make sure that the locked package is looked up in the explicit source although the DependencyCache is not used for locked packages, so we can't rely on it to propagate the correct source. """ package.add_dependency( Factory.create_dependency( "lib", { "version": "1.0", "extras": ["extra"], "source": "explicit1", "markers": "sys_platform != 'linux'", }, ) ) package.add_dependency( Factory.create_dependency( "lib", { "version": "2.0", "extras": ["extra"], "source": "explicit2", "markers": "sys_platform == 'linux'", }, ) ) package.add_dependency(Factory.create_dependency("other", {"version": ">=1.0"})) explicit_repo1 = Repository("explicit1") pool.add_repository(explicit_repo1, priority=Priority.EXPLICIT) explicit_repo2 = Repository("explicit2") pool.add_repository(explicit_repo2, priority=Priority.EXPLICIT) dep_extra = get_dependency("extra", ">=1.0") dep_extra_opt = Factory.create_dependency( "extra", {"version": ">=1.0", "optional": True} ) package_lib1 = Package( "lib", "1.0", source_type="legacy", source_reference="explicit1" ) package_lib1.extras = {canonicalize_name("extra"): [dep_extra]} package_lib1.add_dependency(dep_extra_opt) explicit_repo1.add_package(package_lib1) package_lib2 = Package( "lib", "2.0", source_type="legacy", source_reference="explicit2" ) package_lib2.extras = {canonicalize_name("extra"): [dep_extra]} package_lib2.add_dependency(dep_extra_opt) explicit_repo2.add_package(package_lib2) package_extra = Package("extra", "1.0") repo.add_package(package_extra) package_other = Package("other", "1.5") package_other.add_dependency(Factory.create_dependency("lib", ">=1.0")) repo.add_package(package_other) if locked: locked_packages = [package_extra, package_lib1, package_lib2, package_other] use_latest = [canonicalize_name("other")] else: locked_packages = [] use_latest = None solver = Solver(package, pool, [], locked_packages, io) transaction = solver.solve(use_latest=use_latest) ops = check_solver_result( transaction, [ {"job": "install", "package": package_extra}, {"job": "install", "package": package_lib1}, {"job": "install", "package": package_lib2}, {"job": "install", "package": package_other}, ], ) assert ops[1].package.source_reference == "explicit1" assert ops[2].package.source_reference == "explicit2" @pytest.mark.parametrize("locked", [False, True]) def test_multiple_constraints_incomplete_explicit_source_transitive_locked( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, locked: bool, ) -> None: """ The root package depends on * lib == 1.0+cu ; sys_platform == "linux" with source=explicit * lib == 1.0 ; sys_platform == "darwin" with no explicit source * other >= 1.0 "other" depends on "lib" Since the source for lib 1.0+cu has the priority "explicit", the default source must be chosen for lib 1.0. Since the multiple constraints are incomplete - they are only defined for linux and darwin, there is another hidden override that also requires lib via other. In this hidden override lib 1.0 from the default source must be chosen (because the other source has the priority "explicit"). """ package.add_dependency( Factory.create_dependency( "lib", { "version": "1.0+cu", "source": "explicit", "markers": "sys_platform == 'linux'", }, ) ) package.add_dependency( Factory.create_dependency( "lib", { "version": "1.0", "markers": "sys_platform == 'darwin'", }, ) ) package.add_dependency(Factory.create_dependency("other", {"version": ">=1.0"})) explicit_repo = Repository("explicit") pool.add_repository(explicit_repo, priority=Priority.EXPLICIT) package_lib_explicit = Package( "lib", "1.0+cu", source_type="legacy", source_reference="explicit" ) explicit_repo.add_package(package_lib_explicit) package_lib_default = Package("lib", "1.0") repo.add_package(package_lib_default) package_other = Package("other", "1.5") package_other.add_dependency(Factory.create_dependency("lib", ">=1.0")) repo.add_package(package_other) if locked: # order does not matter because packages are sorted in the provider # (latest first) so that the package from the explicit source is preferred locked_packages = [package_lib_default, package_lib_explicit, package_other] else: locked_packages = [] solver = Solver(package, pool, [], locked_packages, io) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_lib_default}, {"job": "install", "package": package_lib_explicit}, {"job": "install", "package": package_other}, ], ) def test_solver_discards_packages_with_empty_markers( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, ) -> None: package.python_versions = "~2.7 || ^3.4" package.add_dependency( Factory.create_dependency( "a", {"version": "^0.1.0", "markers": "python_version >= '3.4'"} ) ) package_a = get_package("a", "0.1.0") package_a.add_dependency( Factory.create_dependency( "b", {"version": "^0.1.0", "markers": "python_version < '3.2'"} ) ) package_a.add_dependency(Factory.create_dependency("c", "^0.2.0")) package_b = get_package("b", "0.1.0") package_c = get_package("c", "0.2.0") repo.add_package(package_a) repo.add_package(package_b) repo.add_package(package_c) solver = Solver(package, pool, [], [], io) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_c}, {"job": "install", "package": package_a}, ], ) def test_solver_does_not_raise_conflict_for_conditional_dev_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency( Factory.create_dependency( "A", {"version": "^1.0", "python": "~2.7"}, groups=["dev"] ) ) package.add_dependency( Factory.create_dependency( "A", {"version": "^2.0", "python": "^3.5"}, groups=["dev"] ) ) package_a100 = get_package("A", "1.0.0") package_a200 = get_package("A", "2.0.0") repo.add_package(package_a100) repo.add_package(package_a200) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a100}, {"job": "install", "package": package_a200}, ], ) def test_solver_does_not_loop_indefinitely_on_duplicate_constraints_with_extras( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency( Factory.create_dependency( "requests", {"version": "^2.22.0", "extras": ["security"]} ) ) requests = get_package("requests", "2.22.0") requests.add_dependency(Factory.create_dependency("idna", ">=2.5,<2.9")) requests.add_dependency( Factory.create_dependency( "idna", {"version": ">=2.0.0", "markers": "extra == 'security'"} ) ) requests.extras = { canonicalize_name("security"): [get_dependency("idna", ">=2.0.0")] } idna = get_package("idna", "2.8") repo.add_package(requests) repo.add_package(idna) transaction = solver.solve() check_solver_result( transaction, [{"job": "install", "package": idna}, {"job": "install", "package": requests}], ) def test_solver_does_not_fail_with_locked_git_and_non_git_dependencies( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, ) -> None: package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) package.add_dependency(Factory.create_dependency("a", "^1.2.3")) git_package = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference=MOCK_DEFAULT_GIT_REVISION, ) repo.add_package(get_package("a", "1.2.3")) repo.add_package(Package("pendulum", "2.1.2")) installed = [git_package] locked = [get_package("a", "1.2.3"), git_package] solver = Solver(package, pool, installed, locked, io) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": get_package("a", "1.2.3")}, {"job": "install", "package": git_package, "skipped": True}, ], ) def test_ignore_python_constraint_no_overlap_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: pytest = get_package("demo", "1.0.0") pytest.add_dependency( Factory.create_dependency( "configparser", {"version": "^1.2.3", "python": "<3.2"} ) ) package.add_dependency( Factory.create_dependency("demo", {"version": "^1.0.0", "python": "^3.6"}) ) repo.add_package(pytest) repo.add_package(get_package("configparser", "1.2.3")) transaction = solver.solve() check_solver_result( transaction, [{"job": "install", "package": pytest}], ) def test_solver_should_not_go_into_an_infinite_loop_on_duplicate_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency(Factory.create_dependency("A", "^1.0")) package_a = get_package("A", "1.0.0") package_a.add_dependency(Factory.create_dependency("B", "*")) package_a.add_dependency( Factory.create_dependency( "B", {"version": "^1.0", "markers": "implementation_name == 'pypy'"} ) ) package_b20 = get_package("B", "2.0.0") package_b10 = get_package("B", "1.0.0") repo.add_package(package_a) repo.add_package(package_b10) repo.add_package(package_b20) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_b10}, {"job": "install", "package": package_b20}, {"job": "install", "package": package_a}, ], ) def test_solver_synchronize_single( package: ProjectPackage, pool: RepositoryPool, io: NullIO ) -> None: package_a = get_package("a", "1.0") solver = Solver(package, pool, [package_a], [], io) transaction = solver.solve() check_solver_result( transaction, [{"job": "remove", "package": package_a}], synchronize=True ) def test_solver_cannot_choose_another_version_for_directory_dependencies( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pendulum = get_package("pendulum", "2.0.3") demo = get_package("demo", "0.1.0") foo = get_package("foo", "1.2.3") foo.add_dependency(Factory.create_dependency("demo", "<0.1.2")) repo.add_package(foo) repo.add_package(demo) repo.add_package(pendulum) project_dir = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project" ) path = project_dir.as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) package.add_dependency(Factory.create_dependency("foo", "^1.2.3")) # This is not solvable since the demo version is pinned # via the directory dependency with pytest.raises(SolverProblemError): solver.solve() def test_solver_cannot_choose_another_version_for_file_dependencies( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: pendulum = get_package("pendulum", "2.0.3") demo = get_package("demo", "0.0.8") foo = get_package("foo", "1.2.3") foo.add_dependency(Factory.create_dependency("demo", "<0.1.0")) repo.add_package(foo) repo.add_package(demo) repo.add_package(pendulum) project_dir = tmp_path / "project" project_dir.mkdir() path = shutil.copy( fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl", project_dir ) package.add_dependency(Factory.create_dependency("demo", {"path": path})) package.add_dependency(Factory.create_dependency("foo", "^1.2.3")) # This is not solvable since the demo version is pinned # via the file dependency with pytest.raises(SolverProblemError): solver.solve() def test_solver_cannot_choose_another_version_for_git_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: pendulum = get_package("pendulum", "2.0.3") demo = get_package("demo", "0.0.8") foo = get_package("foo", "1.2.3") foo.add_dependency(Factory.create_dependency("demo", "<0.1.0")) repo.add_package(foo) repo.add_package(demo) repo.add_package(pendulum) package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) package.add_dependency(Factory.create_dependency("foo", "^1.2.3")) # This is not solvable since the demo version is pinned # via the file dependency with pytest.raises(SolverProblemError): solver.solve() def test_solver_cannot_choose_another_version_for_url_dependencies( solver: Solver, repo: Repository, package: ProjectPackage, http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: project_dir = tmp_path / "project" project_dir.mkdir() path = shutil.copy( fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl", project_dir ) http.get( "https://files.pythonhosted.org/demo-0.1.0-py2.py3-none-any.whl", body=Path(path).read_bytes(), ) pendulum = get_package("pendulum", "2.0.3") demo = get_package("demo", "0.0.8") foo = get_package("foo", "1.2.3") foo.add_dependency(Factory.create_dependency("demo", "<0.1.0")) repo.add_package(foo) repo.add_package(demo) repo.add_package(pendulum) package.add_dependency( Factory.create_dependency( "demo", { "url": "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" }, ) ) package.add_dependency(Factory.create_dependency("foo", "^1.2.3")) # This is not solvable since the demo version is pinned # via the git dependency with pytest.raises(SolverProblemError): solver.solve() @pytest.mark.parametrize("explicit_source", [True, False]) def test_solver_cannot_choose_url_dependency_for_explicit_source( solver: Solver, repo: Repository, package: ProjectPackage, explicit_source: bool, ) -> None: """A direct origin dependency cannot satisfy a version dependency with an explicit source. (It can satisfy a version dependency without an explicit source.) """ package.add_dependency( Factory.create_dependency( "demo", { "markers": "sys_platform != 'darwin'", "url": "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl", }, ) ) package.add_dependency( Factory.create_dependency( "demo", { "version": "0.1.0", "markers": "sys_platform == 'darwin'", "source": "repo" if explicit_source else None, }, ) ) package_pendulum = get_package("pendulum", "1.4.4") package_demo = Package( "demo", "0.1.0", source_type="legacy" if explicit_source else None, source_reference="repo" if explicit_source else None, ) package_demo_url = Package( "demo", "0.1.0", source_type="url", source_url="https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl", ) # The url demo dependency depends on pendulum. repo.add_package(package_pendulum) repo.add_package(package_demo) transaction = solver.solve() if explicit_source: # direct origin cannot satisfy explicit source # -> package_demo MUST be included expected = [ {"job": "install", "package": package_pendulum}, {"job": "install", "package": package_demo_url}, {"job": "install", "package": package_demo}, ] else: # direct origin can satisfy dependency without source # -> package_demo NEED NOT (but could) be included expected = [ {"job": "install", "package": package_pendulum}, {"job": "install", "package": package_demo_url}, ] check_solver_result(transaction, expected) def test_solver_should_not_update_same_version_packages_if_installed_has_no_source_type( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: package.add_dependency(Factory.create_dependency("foo", "1.0.0")) foo = Package( "foo", "1.0.0", source_type="legacy", source_url="https://foo.bar", source_reference="custom", ) repo.add_package(foo) solver = Solver(package, pool, [get_package("foo", "1.0.0")], [], io) transaction = solver.solve() check_solver_result( transaction, [{"job": "install", "package": foo, "skipped": True}] ) def test_solver_should_use_the_python_constraint_from_the_environment_if_available( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency(Factory.create_dependency("A", "^1.0")) a = get_package("A", "1.0.0") a.add_dependency( Factory.create_dependency( "B", {"version": "^1.0.0", "markers": 'python_version < "3.2"'} ) ) b = get_package("B", "1.0.0") b.python_versions = ">=2.6, <3" repo.add_package(a) repo.add_package(b) with solver.use_environment(MockEnv((2, 7, 18))): transaction = solver.solve() check_solver_result( transaction, [{"job": "install", "package": b}, {"job": "install", "package": a}], ) def test_solver_should_resolve_all_versions_for_multiple_duplicate_dependencies( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.python_versions = "~2.7 || ^3.5" package.add_dependency( Factory.create_dependency( "A", {"version": "^1.0", "markers": "python_version < '3.5'"} ) ) package.add_dependency( Factory.create_dependency( "A", {"version": "^2.0", "markers": "python_version >= '3.5'"} ) ) package.add_dependency( Factory.create_dependency( "B", {"version": "^3.0", "markers": "python_version < '3.5'"} ) ) package.add_dependency( Factory.create_dependency( "B", {"version": "^4.0", "markers": "python_version >= '3.5'"} ) ) package_a10 = get_package("A", "1.0.0") package_a20 = get_package("A", "2.0.0") package_b30 = get_package("B", "3.0.0") package_b40 = get_package("B", "4.0.0") repo.add_package(package_a10) repo.add_package(package_a20) repo.add_package(package_b30) repo.add_package(package_b40) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": package_a10}, {"job": "install", "package": package_a20}, {"job": "install", "package": package_b30}, {"job": "install", "package": package_b40}, ], ) def test_solver_should_not_raise_errors_for_irrelevant_python_constraints( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.python_versions = "^3.6" set_package_python_versions(solver.provider, "^3.6") package.add_dependency( Factory.create_dependency("dataclasses", {"version": "^0.7", "python": "<3.7"}) ) dataclasses = get_package("dataclasses", "0.7") dataclasses.python_versions = ">=3.6, <3.7" repo.add_package(dataclasses) transaction = solver.solve() check_solver_result(transaction, [{"job": "install", "package": dataclasses}]) def test_solver_can_resolve_transitive_extras( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency(Factory.create_dependency("requests", "^2.24.0")) package.add_dependency(Factory.create_dependency("PyOTA", "^2.1.0")) requests = get_package("requests", "2.24.0") requests.add_dependency(Factory.create_dependency("certifi", ">=2017.4.17")) dep = get_dependency("PyOpenSSL", ">=0.14") requests.add_dependency( Factory.create_dependency("PyOpenSSL", {"version": ">=0.14", "optional": True}) ) requests.extras = {canonicalize_name("security"): [dep]} pyota = get_package("PyOTA", "2.1.0") pyota.add_dependency( Factory.create_dependency( "requests", {"version": ">=2.24.0", "extras": ["security"]} ) ) repo.add_package(requests) repo.add_package(pyota) repo.add_package(get_package("certifi", "2017.4.17")) repo.add_package(get_package("pyopenssl", "0.14")) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": get_package("certifi", "2017.4.17")}, {"job": "install", "package": get_package("pyopenssl", "0.14")}, {"job": "install", "package": requests}, {"job": "install", "package": pyota}, ], ) def test_solver_can_resolve_for_packages_with_missing_extras( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: package.add_dependency( Factory.create_dependency( "django-anymail", {"version": "^6.0", "extras": ["postmark"]} ) ) django_anymail = get_package("django-anymail", "6.1.0") django_anymail.add_dependency(Factory.create_dependency("django", ">=2.0")) django_anymail.add_dependency(Factory.create_dependency("requests", ">=2.4.3")) django_anymail.add_dependency( Factory.create_dependency("boto3", {"version": "*", "optional": True}) ) django_anymail.extras = { canonicalize_name("amazon_ses"): [Factory.create_dependency("boto3", "*")] } django = get_package("django", "2.2.0") boto3 = get_package("boto3", "1.0.0") requests = get_package("requests", "2.24.0") repo.add_package(django_anymail) repo.add_package(django) repo.add_package(boto3) repo.add_package(requests) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": django}, {"job": "install", "package": requests}, {"job": "install", "package": django_anymail}, ], ) def test_solver_can_resolve_python_restricted_package_dependencies( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: package.add_dependency( Factory.create_dependency("futures", {"version": "^3.3.0", "python": "~2.7"}) ) package.add_dependency( Factory.create_dependency("pre-commit", {"version": "^2.6", "python": "^3.6.1"}) ) futures = Package("futures", "3.3.0") futures.python_versions = ">=2.6, <3" pre_commit = Package("pre-commit", "2.7.1") pre_commit.python_versions = ">=3.6.1" repo.add_package(futures) repo.add_package(pre_commit) solver = Solver(package, pool, [], [futures, pre_commit], io) transaction = solver.solve(use_latest=[canonicalize_name("pre-commit")]) check_solver_result( transaction, [ {"job": "install", "package": futures}, {"job": "install", "package": pre_commit}, ], ) @pytest.mark.parametrize("virtualenv_before_pre_commit", [False, True]) def test_solver_should_not_raise_errors_for_irrelevant_transitive_python_constraints( solver: Solver, repo: Repository, package: ProjectPackage, mocker: MockerFixture, virtualenv_before_pre_commit: bool, ) -> None: package.python_versions = "~2.7 || ^3.5" set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency(Factory.create_dependency("virtualenv", "^20.4.3")) package.add_dependency( Factory.create_dependency("pre-commit", {"version": "^2.6", "python": "^3.6.1"}) ) virtualenv = get_package("virtualenv", "20.4.3") virtualenv.python_versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" virtualenv.add_dependency( Factory.create_dependency( "importlib-resources", {"version": "*", "markers": 'python_version < "3.7"'} ) ) pre_commit = Package("pre-commit", "2.7.1") pre_commit.python_versions = ">=3.6.1" pre_commit.add_dependency( Factory.create_dependency( "importlib-resources", {"version": "*", "markers": 'python_version < "3.7"'} ) ) importlib_resources = get_package("importlib-resources", "5.1.2") importlib_resources.python_versions = ">=3.6" importlib_resources_3_2_1 = get_package("importlib-resources", "3.2.1") importlib_resources_3_2_1.python_versions = ( "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" ) repo.add_package(virtualenv) repo.add_package(pre_commit) repo.add_package(importlib_resources) repo.add_package(importlib_resources_3_2_1) def patched_choose_next(unsatisfied: list[Dependency]) -> Dependency: order = ( ("root", "virtualenv", "pre-commit", "importlib-resources") if virtualenv_before_pre_commit else ("root", "pre-commit", "virtualenv", "importlib-resources") ) for preferred in order: for dep in unsatisfied: if dep.name == preferred: return dep raise RuntimeError mocker.patch( "poetry.mixology.version_solver.VersionSolver._choose_next", side_effect=patched_choose_next, ) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": importlib_resources_3_2_1}, {"job": "install", "package": pre_commit}, {"job": "install", "package": virtualenv}, ], ) @pytest.mark.parametrize("numpy_before_pandas", [False, True]) @pytest.mark.parametrize("conflict", [False, True]) def test_solver_should_not_raise_errors_for_irrelevant_transitive_python_constraints2( solver: Solver, repo: Repository, package: ProjectPackage, mocker: MockerFixture, numpy_before_pandas: bool, conflict: bool, ) -> None: """This time with overrides.""" package.python_versions = ">=3.6.2, <3.10" set_package_python_versions(solver.provider, ">=3.6.2, <3.10") package.add_dependency(Factory.create_dependency("pandas", ">=1")) package.add_dependency( Factory.create_dependency("numpy", {"version": ">=1.20.0", "python": ">=3.7"}) ) package.add_dependency( Factory.create_dependency("numpy", {"version": "*", "python": "<3.7"}) ) pandas = get_package("pandas", "1.1.5") pandas.add_dependency(Factory.create_dependency("numpy", ">=1.15")) numpy_19 = get_package("numpy", "1.19") numpy_19.python_versions = ">=3.6" numpy_20 = get_package("numpy", "1.20") numpy_20.python_versions = ">=3.8" if conflict else ">=3.7" repo.add_package(pandas) repo.add_package(numpy_19) repo.add_package(numpy_20) def patched_choose_next(unsatisfied: list[Dependency]) -> Dependency: order = ( ("root", "pandas", "numpy") if numpy_before_pandas else ("root", "numpy", "pandas") ) for preferred in order: for dep in unsatisfied: if dep.name == preferred: return dep raise RuntimeError mocker.patch( "poetry.mixology.version_solver.VersionSolver._choose_next", side_effect=patched_choose_next, ) if conflict: with pytest.raises(SolverProblemError): solver.solve() else: transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": numpy_19}, {"job": "install", "package": numpy_20}, {"job": "install", "package": pandas}, ], ) @pytest.mark.parametrize("is_locked", [False, True]) def test_solver_keeps_multiple_locked_dependencies_for_same_package( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, is_locked: bool, ) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "~1.1", "python": "<3.7"}) ) package.add_dependency( Factory.create_dependency("A", {"version": "~1.2", "python": ">=3.7"}) ) a11 = Package("A", "1.1") a12 = Package("A", "1.2") a11.add_dependency(Factory.create_dependency("B", {"version": ">=0.3"})) a12.add_dependency(Factory.create_dependency("B", {"version": ">=0.3"})) b03 = Package("B", "0.3") b04 = Package("B", "0.4") b04.python_versions = ">=3.6.2,<4.0.0" repo.add_package(a11) repo.add_package(a12) repo.add_package(b03) repo.add_package(b04) if is_locked: a11_locked = a11.clone() a11_locked.python_versions = "<3.7" a12_locked = a12.clone() a12_locked.python_versions = ">=3.7" locked = [a11_locked, a12_locked, b03.clone(), b04.clone()] else: locked = [] solver = Solver(package, pool, [], locked, io) set_package_python_versions(solver.provider, "^3.6") transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": b03}, {"job": "install", "package": b04}, {"job": "install", "package": a11}, {"job": "install", "package": a12}, ], ) @pytest.mark.parametrize("is_locked", [False, True]) def test_solver_does_not_update_ref_of_locked_vcs_package( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, is_locked: bool, ) -> None: locked_ref = "123456" latest_ref = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24" demo_locked = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference=locked_ref, ) demo_locked.add_dependency(Factory.create_dependency("pendulum", "*")) demo_latest = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference=latest_ref, ) locked = [demo_locked] if is_locked else [] package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) # transitive dependencies of demo pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) solver = Solver(package, pool, [], locked, io) transaction = solver.solve() ops = check_solver_result( transaction, [ {"job": "install", "package": pendulum}, {"job": "install", "package": demo_locked if is_locked else demo_latest}, ], ) op = ops[1] assert op.package.source_type == "git" assert op.package.source_reference == DEFAULT_SOURCE_REF assert ( op.package.source_resolved_reference == locked_ref if is_locked else latest_ref ) def test_solver_does_not_fetch_locked_vcs_package_with_ref( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, mocker: MockerFixture, ) -> None: locked_ref = "123456" demo_locked = Package( "demo", "0.1.2", source_type="git", source_url="https://github.com/demo/demo.git", source_reference=DEFAULT_SOURCE_REF, source_resolved_reference=locked_ref, ) demo_locked.add_dependency(Factory.create_dependency("pendulum", "*")) package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) # transitive dependencies of demo pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) solver = Solver(package, pool, [], [demo_locked], io) spy = mocker.spy(solver._provider, "_search_for_vcs") solver.solve() spy.assert_not_called() def test_solver_direct_origin_dependency_with_extras_requested_by_other_package( solver: Solver, repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, tmp_path: Path, ) -> None: """ Another package requires the same dependency with extras that is required by the project as direct origin dependency without any extras. """ pendulum = get_package("pendulum", "2.0.3") # required by demo cleo = get_package("cleo", "1.0.0") # required by demo[foo] demo_foo = get_package("demo-foo", "1.2.3") demo_foo.add_dependency( Factory.create_dependency("demo", {"version": ">=0.1", "extras": ["foo"]}) ) repo.add_package(demo_foo) repo.add_package(pendulum) repo.add_package(cleo) project_dir = shutil.copytree( fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project", ) path = project_dir.as_posix() # project requires path dependency of demo while demo-foo requires demo[foo] package.add_dependency(Factory.create_dependency("demo", {"path": path})) package.add_dependency(Factory.create_dependency("demo-foo", "^1.2.3")) transaction = solver.solve() demo = Package("demo", "0.1.2", source_type="directory", source_url=path) ops = check_solver_result( transaction, [ {"job": "install", "package": cleo}, {"job": "install", "package": pendulum}, {"job": "install", "package": demo}, {"job": "install", "package": demo_foo}, ], ) op = ops[2] assert op.package.name == "demo" assert op.package.version.text == "0.1.2" assert op.package.source_type == "directory" assert op.package.source_url == path def test_solver_incompatible_dependency_with_and_without_extras( solver: Solver, repo: Repository, package: ProjectPackage ) -> None: """ The solver first encounters a requirement for google-auth and then later an incompatible requirement for google-auth[aiohttp]. Testcase derived from https://github.com/python-poetry/poetry/issues/6054. """ # Incompatible requirements from foo and bar2. foo = get_package("foo", "1.0.0") foo.add_dependency(Factory.create_dependency("google-auth", {"version": "^1"})) bar = get_package("bar", "1.0.0") bar2 = get_package("bar", "2.0.0") bar2.add_dependency( Factory.create_dependency( "google-auth", {"version": "^2", "extras": ["aiohttp"]} ) ) baz = get_package("baz", "1.0.0") # required by google-auth[aiohttp] google_auth = get_package("google-auth", "1.2.3") google_auth.extras = {canonicalize_name("aiohttp"): [get_dependency("baz", "^1.0")]} google_auth2 = get_package("google-auth", "2.3.4") google_auth2.extras = { canonicalize_name("aiohttp"): [get_dependency("baz", "^1.0")] } repo.add_package(foo) repo.add_package(bar) repo.add_package(bar2) repo.add_package(baz) repo.add_package(google_auth) repo.add_package(google_auth2) package.add_dependency(Factory.create_dependency("foo", ">=1")) package.add_dependency(Factory.create_dependency("bar", ">=1")) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": google_auth}, {"job": "install", "package": bar}, {"job": "install", "package": foo}, ], ) def test_update_with_prerelease_and_no_solution( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO ) -> None: # Locked and installed: cleo which depends on an old version of crashtest. cleo = get_package("cleo", "1.0.0a5") crashtest = get_package("crashtest", "0.3.0") cleo.add_dependency(Factory.create_dependency("crashtest", {"version": "<0.4.0"})) installed = [cleo, crashtest] locked = [cleo, crashtest] # Try to upgrade to a new version of crashtest, this will be disallowed by the # dependency from cleo. package.add_dependency(Factory.create_dependency("cleo", "^1.0.0a5")) package.add_dependency(Factory.create_dependency("crashtest", "^0.4.0")) newer_crashtest = get_package("crashtest", "0.4.0") even_newer_crashtest = get_package("crashtest", "0.4.1") repo.add_package(cleo) repo.add_package(crashtest) repo.add_package(newer_crashtest) repo.add_package(even_newer_crashtest) solver = Solver(package, pool, installed, locked, io) with pytest.raises(SolverProblemError): solver.solve() def test_solver_yanked_warning( package: ProjectPackage, pool: RepositoryPool, repo: Repository, ) -> None: package.add_dependency(Factory.create_dependency("foo", "==1")) package.add_dependency(Factory.create_dependency("bar", "==2")) package.add_dependency(Factory.create_dependency("baz", "==3")) foo = get_package("foo", "1", yanked=False) bar = get_package("bar", "2", yanked=True) baz = get_package("baz", "3", yanked="just wrong") repo.add_package(foo) repo.add_package(bar) repo.add_package(baz) io = BufferedIO(decorated=False) solver = Solver(package, pool, [], [], io) transaction = solver.solve() check_solver_result( transaction, [ {"job": "install", "package": bar}, {"job": "install", "package": baz}, {"job": "install", "package": foo}, ], ) error = io.fetch_error() assert "foo" not in error assert "The locked version 2 for bar is a yanked version." in error assert ( "The locked version 3 for baz is a yanked version. Reason for being yanked:" " just wrong" in error ) assert error.count("is a yanked version") == 2 assert error.count("Reason for being yanked") == 1 @pytest.mark.parametrize("is_locked", [False, True]) def test_update_with_use_latest_vs_lock( package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO, is_locked: bool, ) -> None: """ A1 depends on B2, A2 and A3 depend on B1. Same for C. B1 depends on A2/C2, B2 depends on A1/C1. Because there are more versions of B than of A and C, B is resolved first so that latest version of B is used. There shouldn't be a difference between `poetry lock` (not is_locked) and `poetry update` (is_locked + use_latest) """ # B added between A and C (and also alphabetically between) # to ensure that neither the first nor the last one is resolved first package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency(Factory.create_dependency("C", "*")) package_a1 = get_package("A", "1") package_a1.add_dependency(Factory.create_dependency("B", "3")) package_a2 = get_package("A", "2") package_a2.add_dependency(Factory.create_dependency("B", "1")) package_c1 = get_package("C", "1") package_c1.add_dependency(Factory.create_dependency("B", "3")) package_c2 = get_package("C", "2") package_c2.add_dependency(Factory.create_dependency("B", "1")) package_b1 = get_package("B", "1") package_b1.add_dependency(Factory.create_dependency("A", "2")) package_b1.add_dependency(Factory.create_dependency("C", "2")) package_b2 = get_package("B", "2") package_b2.add_dependency(Factory.create_dependency("A", "1")) package_b2.add_dependency(Factory.create_dependency("C", "1")) package_b3 = get_package("B", "3") package_b3.add_dependency(Factory.create_dependency("A", "1")) package_b3.add_dependency(Factory.create_dependency("C", "1")) repo.add_package(package_a1) repo.add_package(package_a2) repo.add_package(package_b1) repo.add_package(package_b2) repo.add_package(package_b3) repo.add_package(package_c1) repo.add_package(package_c2) if is_locked: locked = [package_a1, package_b3, package_c1] use_latest = [package.name for package in locked] else: locked = [] use_latest = [] solver = Solver(package, pool, [], locked, io) transaction = solver.solve(use_latest) check_solver_result( transaction, [ {"job": "install", "package": package_c1}, {"job": "install", "package": package_b3}, {"job": "install", "package": package_a1}, ], ) @pytest.mark.parametrize("with_extra", [False, True]) def test_solver_resolves_duplicate_dependency_in_extra( package: ProjectPackage, pool: RepositoryPool, repo: Repository, io: NullIO, with_extra: bool, ) -> None: """ Without extras, a newer version of B can be chosen than with extras. See https://github.com/python-poetry/poetry/issues/8380. """ constraint: dict[str, Any] = {"version": "*"} if with_extra: constraint["extras"] = ["foo"] package.add_dependency(Factory.create_dependency("A", constraint)) package_a = get_package("A", "1.0") package_b1 = get_package("B", "1.0") package_b2 = get_package("B", "2.0") dep = get_dependency("B", ">=1.0") package_a.add_dependency(dep) dep_extra = get_dependency("B", "^1.0", optional=True) dep_extra.marker = parse_marker("extra == 'foo'") package_a.extras = {canonicalize_name("foo"): [dep_extra]} package_a.add_dependency(dep_extra) repo.add_package(package_a) repo.add_package(package_b1) repo.add_package(package_b2) solver = Solver(package, pool, [], [], io) transaction = solver.solve() check_solver_result( transaction, ( [ {"job": "install", "package": package_b1 if with_extra else package_b2}, {"job": "install", "package": package_a}, ] ), ) def test_solver_resolves_conflicting_dependency_in_root_extra( package: ProjectPackage, pool: RepositoryPool, repo: Repository, io: NullIO, ) -> None: package_a1 = get_package("A", "1.0") package_a2 = get_package("A", "2.0") dep = get_dependency("A", {"version": "1.0", "markers": "extra != 'foo'"}) package.add_dependency(dep) dep_extra = get_dependency("A", "2.0", optional=True) dep_extra._in_extras = [canonicalize_name("foo")] package.extras = {canonicalize_name("foo"): [dep_extra]} package.add_dependency(dep_extra) repo.add_package(package_a1) repo.add_package(package_a2) solver = Solver(package, pool, [], [], io) transaction = solver.solve() check_solver_result( transaction, ( [ {"job": "install", "package": package_a1}, {"job": "install", "package": package_a2}, ] ), ) solved_packages = transaction.get_solved_packages() assert solved_packages[package_a1].markers[MAIN_GROUP] == parse_marker( "extra != 'foo'" ) assert solved_packages[package_a2].markers[MAIN_GROUP] == parse_marker( "extra == 'foo'" ) def test_solver_resolves_conflicting_dependency_in_root_extras( package: ProjectPackage, pool: RepositoryPool, repo: Repository, io: NullIO, ) -> None: package_a1 = get_package("A", "1.0") package_a2 = get_package("A", "2.0") dep_extra1 = get_dependency( # extra == 'foo' is implicit via _in_extras! "A", {"version": "1.0", "markers": "extra != 'bar'"}, optional=True ) dep_extra1._in_extras = [canonicalize_name("foo")] package.add_dependency(dep_extra1) dep_extra2 = get_dependency( # extra == 'bar' is implicit via _in_extras! "A", {"version": "2.0", "markers": "extra != 'foo'"}, optional=True ) dep_extra2._in_extras = [canonicalize_name("bar")] package.extras = { canonicalize_name("foo"): [dep_extra1], canonicalize_name("bar"): [dep_extra2], } package.add_dependency(dep_extra1) package.add_dependency(dep_extra2) repo.add_package(package_a1) repo.add_package(package_a2) solver = Solver(package, pool, [], [], io) transaction = solver.solve() check_solver_result( transaction, ( [ {"job": "install", "package": package_a1}, {"job": "install", "package": package_a2}, ] ), ) solved_packages = transaction.get_solved_packages() assert solved_packages[package_a1].markers[MAIN_GROUP] == parse_marker( "extra != 'bar' and extra == 'foo'" ) assert solved_packages[package_a2].markers[MAIN_GROUP] == parse_marker( "extra != 'foo' and extra == 'bar'" ) @pytest.mark.parametrize("with_extra", [False, True]) def test_solver_resolves_duplicate_dependency_in_root_extra_for_installation( package: ProjectPackage, pool: RepositoryPool, repo: Repository, io: NullIO, with_extra: bool, ) -> None: """ Without extras, a newer version of A can be chosen than with root extras. """ extra = [canonicalize_name("foo")] if with_extra else [] package_a1 = get_package("A", "1.0") package_a2 = get_package("A", "2.0") dep = get_dependency("A", ">=1.0") package.add_dependency(dep) dep_extra = get_dependency("A", "^1.0", optional=True) dep_extra.marker = parse_marker("extra == 'foo'") package.extras = {canonicalize_name("foo"): [dep_extra]} package.add_dependency(dep_extra) repo.add_package(package_a1) repo.add_package(package_a2) solver = Solver( package, pool, [], [package_a1, package_a2], io, active_root_extras=extra ) with solver.use_environment(MockEnv()): transaction = solver.solve() check_solver_result( transaction, ( [ {"job": "install", "package": package_a1 if with_extra else package_a2}, ] ), ) def test_solver_resolves_duplicate_dependencies_with_restricted_extras( package: ProjectPackage, pool: RepositoryPool, repo: Repository, io: NullIO, ) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "*", "extras": ["foo"]}) ) package_a = get_package("A", "1.0") package_b1 = get_package("B", "1.0") package_b2 = get_package("B", "2.0") dep1 = get_dependency("B", "^1.0", optional=True) dep1.marker = parse_marker("sys_platform == 'win32' and extra == 'foo'") dep2 = get_dependency("B", "^2.0", optional=True) dep2.marker = parse_marker("sys_platform == 'linux' and extra == 'foo'") package_a.extras = {canonicalize_name("foo"): [dep1, dep2]} package_a.add_dependency(dep1) package_a.add_dependency(dep2) repo.add_package(package_a) repo.add_package(package_b1) repo.add_package(package_b2) solver = Solver(package, pool, [], [], io) transaction = solver.solve() check_solver_result( transaction, ( [ {"job": "install", "package": package_b1}, {"job": "install", "package": package_b2}, {"job": "install", "package": package_a}, ] ), ) ================================================ FILE: tests/puzzle/test_solver_internals.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from packaging.utils import canonicalize_name from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.package import Package from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import parse_marker from poetry.factory import Factory from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.puzzle.solver import PackageNode from poetry.puzzle.solver import Solver from poetry.puzzle.solver import depth_first_search from poetry.puzzle.solver import merge_override_packages if TYPE_CHECKING: from collections.abc import Iterable from collections.abc import Sequence from poetry.core.packages.project_package import ProjectPackage DEV_GROUP = canonicalize_name("dev") def dep( name: str, marker: str = "", extras: Iterable[str] = (), in_extras: Sequence[str] = (), groups: Iterable[str] = (), ) -> Dependency: d = Dependency(name, "1", groups=groups, extras=extras) d._in_extras = [canonicalize_name(e) for e in in_extras] if marker: d.marker = marker return d def tm(info: TransitivePackageInfo) -> dict[str, str]: return {key: str(value) for key, value in info.markers.items()} def test_dfs_depth(package: ProjectPackage) -> None: a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") packages = [package, a, b, c] package.add_dependency(dep("a")) package.add_dependency(dep("b")) a.add_dependency(dep("b")) b.add_dependency(dep("c")) result, __ = depth_first_search(PackageNode(package, packages)) depths = { nodes[0].package.complete_name: [node.depth for node in nodes] for nodes in result } assert depths == {"root": [-1], "a": [0], "b": [1], "c": [2]} def test_dfs_depth_with_cycle(package: ProjectPackage) -> None: a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") packages = [package, a, b, c] package.add_dependency(dep("a")) package.add_dependency(dep("b")) a.add_dependency(dep("b")) b.add_dependency(dep("a")) a.add_dependency(dep("c")) result, __ = depth_first_search(PackageNode(package, packages)) depths = { nodes[0].package.complete_name: [node.depth for node in nodes] for nodes in result } assert depths == {"root": [-1], "a": [0], "b": [1], "c": [1]} def test_dfs_depth_with_extra(package: ProjectPackage) -> None: a_foo = Package("a", "1", features=["foo"]) a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") packages = [package, a_foo, a, b, c] package.add_dependency(dep("a", extras=["foo"])) a_foo.add_dependency(dep("a")) a_foo.add_dependency(dep("b")) a_foo.add_dependency(dep("c", 'extra == "foo"')) a.add_dependency(dep("b")) result, __ = depth_first_search(PackageNode(package, packages)) depths = { nodes[0].package.complete_name: [node.depth for node in nodes] for nodes in result } assert depths == {"root": [-1], "a[foo]": [0], "a": [0], "b": [1], "c": [1]} def test_propagate_markers(package: ProjectPackage, solver: Solver) -> None: a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") d = Package("d", "1") e = Package("e", "1") package.add_dependency(dep("a", 'sys_platform == "win32"')) package.add_dependency(dep("b", 'sys_platform == "linux"')) a.add_dependency(dep("c", 'python_version == "3.8"')) b.add_dependency(dep("d", 'python_version == "3.9"')) a.add_dependency(dep("e", 'python_version == "3.10"')) b.add_dependency(dep("e", 'python_version == "3.11"')) packages = [package, a, b, c, d, e] result = solver._aggregate_solved_packages(packages) assert len(result) == 6 assert tm(result[package]) == {} assert tm(result[a]) == {"main": 'sys_platform == "win32"'} assert tm(result[b]) == {"main": 'sys_platform == "linux"'} assert tm(result[c]) == { "main": 'sys_platform == "win32" and python_version == "3.8"' } assert tm(result[d]) == { "main": 'sys_platform == "linux" and python_version == "3.9"' } assert tm(result[e]) == { "main": 'sys_platform == "win32" and python_version == "3.10"' ' or sys_platform == "linux" and python_version == "3.11"' } def test_propagate_markers_same_name(package: ProjectPackage, solver: Solver) -> None: urls = { "linux": "https://files.pythonhosted.org/distributions/demo-0.1.0.tar.gz", "win32": ( "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" ), } sdist = Package("demo", "0.1.0", source_type="url", source_url=urls["linux"]) wheel = Package("demo", "0.1.0", source_type="url", source_url=urls["win32"]) for platform, url in urls.items(): package.add_dependency( Factory.create_dependency( "demo", {"url": url, "markers": f"sys_platform == '{platform}'"}, ) ) packages = [package, sdist, wheel] result = solver._aggregate_solved_packages(packages) assert len(result) == 3 assert tm(result[package]) == {} assert tm(result[sdist]) == {"main": 'sys_platform == "linux"'} assert tm(result[wheel]) == {"main": 'sys_platform == "win32"'} def test_propagate_markers_with_extra(package: ProjectPackage, solver: Solver) -> None: a_foo = Package("a", "1", features=["foo"]) a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") d = Package("d", "1") package.add_dependency(dep("a", 'sys_platform == "win32"', extras=["foo"])) package.add_dependency(dep("b", 'sys_platform == "linux"')) a_foo.add_dependency(dep("a")) a_foo.add_dependency(dep("c", 'python_version == "3.8"')) a_foo.add_dependency(dep("d", 'extra == "foo"')) a.add_dependency(dep("c", 'python_version == "3.8"')) b.add_dependency(dep("a", 'python_version == "3.9"')) packages = [package, a_foo, a, b, c, d] result = solver._aggregate_solved_packages(packages) assert len(result) == len(packages) - 1 assert tm(result[package]) == {} assert tm(result[a]) == { "main": ( 'sys_platform == "linux" and python_version == "3.9" or sys_platform == "win32"' ) } assert tm(result[b]) == {"main": 'sys_platform == "linux"'} assert tm(result[c]) == { "main": 'sys_platform == "win32" and python_version == "3.8"' } assert tm(result[d]) == {"main": 'sys_platform == "win32"'} def test_propagate_markers_with_root_extra( package: ProjectPackage, solver: Solver ) -> None: a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") d = Package("d", "1") # "extra" is not present in the marker of an extra dependency of the root package, # there is only "in_extras"... package.add_dependency(dep("a", in_extras=["foo"])) package.add_dependency( dep("b", 'sys_platform == "linux"', in_extras=["foo", "bar"]) ) a.add_dependency(dep("c", 'python_version == "3.8"')) b.add_dependency(dep("d", 'python_version == "3.9"')) packages = [package, a, b, c, d] result = solver._aggregate_solved_packages(packages) assert len(result) == len(packages) assert tm(result[package]) == {} assert tm(result[a]) == {"main": 'extra == "foo"'} assert tm(result[b]) == { "main": 'sys_platform == "linux" and (extra == "foo" or extra == "bar")', } assert tm(result[c]) == {"main": 'extra == "foo" and python_version == "3.8"'} assert tm(result[d]) == { "main": ( 'sys_platform == "linux" and (extra == "foo" or extra == "bar")' ' and python_version == "3.9"' ) } def test_propagate_markers_with_duplicate_dependency_root_extra( package: ProjectPackage, solver: Solver ) -> None: a = Package("a", "1") package.add_dependency(dep("a")) # "extra" is not present in the marker of an extra dependency of the root package, # there is only "in_extras"... package.add_dependency(dep("a", in_extras=["foo"])) packages = [package, a] result = solver._aggregate_solved_packages(packages) assert len(result) == len(packages) assert tm(result[package]) == {} assert tm(result[a]) == {"main": ""} # not "extra == 'foo'" ! def test_propagate_groups_with_extra(package: ProjectPackage, solver: Solver) -> None: a_foo = Package("a", "1", features=["foo"]) a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") package.add_dependency(dep("a", groups=["main"])) package.add_dependency(dep("a", groups=["dev"], extras=["foo"])) a_foo.add_dependency(dep("a")) a_foo.add_dependency(dep("b")) a_foo.add_dependency(dep("c", 'extra == "foo"')) a.add_dependency(dep("b")) packages = [package, a_foo, a, b, c] result = solver._aggregate_solved_packages(packages) assert len(result) == len(packages) - 1 assert result[package].groups == set() assert result[a].groups == {"main", "dev"} assert result[b].groups == {"main", "dev"} assert result[c].groups == {"dev"} def test_propagate_markers_for_groups1(package: ProjectPackage, solver: Solver) -> None: a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") package.add_dependency(dep("a", 'sys_platform == "win32"', groups=["main"])) package.add_dependency(dep("b", 'sys_platform == "linux"', groups=["dev"])) a.add_dependency(dep("c", 'python_version == "3.8"')) b.add_dependency(dep("c", 'python_version == "3.9"')) packages = [package, a, b, c] result = solver._aggregate_solved_packages(packages) assert len(result) == len(packages) assert result[package].groups == set() assert result[a].groups == {"main"} assert result[b].groups == {"dev"} assert result[c].groups == {"main", "dev"} assert tm(result[package]) == {} assert tm(result[a]) == {"main": 'sys_platform == "win32"'} assert tm(result[b]) == {"dev": 'sys_platform == "linux"'} assert tm(result[c]) == { "main": 'sys_platform == "win32" and python_version == "3.8"', "dev": 'sys_platform == "linux" and python_version == "3.9"', } def test_propagate_markers_for_groups2(package: ProjectPackage, solver: Solver) -> None: a = Package("a", "1") b = Package("b", "1") c = Package("c", "1") d = Package("d", "1") package.add_dependency(dep("a", 'sys_platform == "win32"', groups=["main"])) package.add_dependency(dep("b", 'sys_platform == "linux"', groups=["dev"])) package.add_dependency(dep("c", 'sys_platform == "darwin"', groups=["main", "dev"])) a.add_dependency(dep("d", 'python_version == "3.8"')) b.add_dependency(dep("d", 'python_version == "3.9"')) c.add_dependency(dep("d", 'python_version == "3.10"')) packages = [package, a, b, c, d] result = solver._aggregate_solved_packages(packages) assert len(result) == len(packages) assert result[package].groups == set() assert result[a].groups == {"main"} assert result[b].groups == {"dev"} assert result[c].groups == {"main", "dev"} assert result[d].groups == {"main", "dev"} assert tm(result[package]) == {} assert tm(result[a]) == {"main": 'sys_platform == "win32"'} assert tm(result[b]) == {"dev": 'sys_platform == "linux"'} assert tm(result[c]) == { "main": 'sys_platform == "darwin"', "dev": 'sys_platform == "darwin"', } assert tm(result[d]) == { "main": ( 'sys_platform == "win32" and python_version == "3.8"' ' or sys_platform == "darwin" and python_version == "3.10"' ), "dev": ( 'sys_platform == "darwin" and python_version == "3.10"' ' or sys_platform == "linux" and python_version == "3.9"' ), } def test_propagate_markers_for_groups_same_dep( package: ProjectPackage, solver: Solver ) -> None: a = Package("a", "1") b = Package("b", "1") package.add_dependency(dep("a", 'sys_platform == "win32"', groups=["main"])) package.add_dependency(dep("a", 'sys_platform == "linux"', groups=["dev"])) a.add_dependency(dep("b", 'python_version == "3.8"')) packages = [package, a, b] result = solver._aggregate_solved_packages(packages) assert len(result) == len(packages) assert result[package].groups == set() assert result[a].groups == {"main", "dev"} assert result[b].groups == {"main", "dev"} assert tm(result[package]) == {} assert tm(result[a]) == { "main": 'sys_platform == "win32"', "dev": 'sys_platform == "linux"', } assert tm(result[b]) == { "main": 'sys_platform == "win32" and python_version == "3.8"', "dev": 'sys_platform == "linux" and python_version == "3.8"', } def test_propagate_markers_for_groups_with_extra( package: ProjectPackage, solver: Solver ) -> None: a = Package("a", "1") package.add_dependency(dep("a", groups=["main"], in_extras=["foo"])) package.add_dependency(dep("a", groups=["dev"])) packages = [package, a] result = solver._aggregate_solved_packages(packages) assert len(result) == len(packages) assert result[package].groups == set() assert result[a].groups == {"main", "dev"} assert tm(result[package]) == {} assert tm(result[a]) == {"main": 'extra == "foo"', "dev": ""} def test_propagate_markers_with_cycle(package: ProjectPackage, solver: Solver) -> None: a = Package("a", "1") b = Package("b", "1") package.add_dependency(dep("a", 'sys_platform == "win32"')) package.add_dependency(dep("b", 'sys_platform == "linux"')) a.add_dependency(dep("b", 'python_version == "3.8"')) b.add_dependency(dep("a", 'python_version == "3.9"')) packages = [package, a, b] result = solver._aggregate_solved_packages(packages) assert len(result) == 3 assert tm(result[package]) == {} assert tm(result[a]) == { "main": ( 'sys_platform == "linux" and python_version == "3.9"' ' or sys_platform == "win32"' ) } assert tm(result[b]) == { "main": ( 'sys_platform == "win32" and python_version == "3.8"' ' or sys_platform == "linux"' ) } def test_merge_override_packages_restricted(package: ProjectPackage) -> None: """Markers of dependencies should be intersected with override markers.""" a = Package("a", "1") packages = merge_override_packages( [ ( {package: {"a": dep("b", 'python_version < "3.9"')}}, { a: TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("sys_platform == 'win32'")}, ) }, ), ( {package: {"a": dep("b", 'python_version >= "3.9"')}}, { a: TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("sys_platform == 'linux'")}, ) }, ), ], parse_constraint("*"), ) assert len(packages) == 1 assert packages[a].groups == {"main"} assert tm(packages[a]) == { "main": ( 'python_version < "3.9" and sys_platform == "win32"' ' or sys_platform == "linux" and python_version >= "3.9"' ) } def test_merge_override_packages_extras(package: ProjectPackage) -> None: """Extras from overrides should not be visible in the resulting marker.""" a = Package("a", "1") packages = merge_override_packages( [ ( {package: {"a": dep("b", 'python_version < "3.9" and extra == "foo"')}}, { a: TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("sys_platform == 'win32'")}, ) }, ), ( { package: { "a": dep("b", 'python_version >= "3.9" and extra == "foo"') } }, { a: TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("sys_platform == 'linux'")}, ) }, ), ], parse_constraint("*"), ) assert len(packages) == 1 assert packages[a].groups == {"main"} assert tm(packages[a]) == { "main": ( 'python_version < "3.9" and sys_platform == "win32"' ' or sys_platform == "linux" and python_version >= "3.9"' ) } @pytest.mark.parametrize( ("python_constraint", "expected"), [ (">=3.8", 'python_version > "3.8" or sys_platform != "linux"'), (">=3.9", ""), ], ) def test_merge_override_packages_python_constraint( package: ProjectPackage, python_constraint: str, expected: str ) -> None: """The resulting marker depends on the project's python constraint.""" a = Package("a", "1") packages = merge_override_packages( [ ( { package: { "a": dep( "b", "sys_platform == 'linux' and python_version > '3.8'" ) } }, {a: TransitivePackageInfo(0, {MAIN_GROUP}, {MAIN_GROUP: AnyMarker()})}, ), ( {package: {"a": dep("b", "sys_platform != 'linux'")}}, {a: TransitivePackageInfo(0, {MAIN_GROUP}, {MAIN_GROUP: AnyMarker()})}, ), ], parse_constraint(python_constraint), ) assert len(packages) == 1 assert packages[a].groups == {"main"} assert tm(packages[a]) == {"main": expected} def test_merge_override_packages_multiple_deps(package: ProjectPackage) -> None: """All override markers should be intersected.""" a = Package("a", "1") packages = merge_override_packages( [ ( { package: { "a": dep("b", 'python_version < "3.9"'), "c": dep("d", 'sys_platform == "linux"'), }, a: {"e": dep("f", 'python_version >= "3.8"')}, }, {a: TransitivePackageInfo(0, {MAIN_GROUP}, {MAIN_GROUP: AnyMarker()})}, ), ], parse_constraint("*"), ) assert len(packages) == 1 assert packages[a].groups == {"main"} assert tm(packages[a]) == { "main": 'python_version == "3.8" and sys_platform == "linux"' } def test_merge_override_packages_groups(package: ProjectPackage) -> None: a = Package("a", "1") b = Package("b", "1") packages = merge_override_packages( [ ( {package: {"a": dep("b", 'python_version < "3.9"')}}, { a: TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("sys_platform == 'win32'")}, ), b: TransitivePackageInfo( 0, {MAIN_GROUP, DEV_GROUP}, { MAIN_GROUP: parse_marker("sys_platform == 'win32'"), DEV_GROUP: parse_marker("sys_platform == 'linux'"), }, ), }, ), ( {package: {"a": dep("b", 'python_version >= "3.9"')}}, { a: TransitivePackageInfo( 0, {DEV_GROUP}, {DEV_GROUP: parse_marker("sys_platform == 'linux'")}, ), b: TransitivePackageInfo( 0, {MAIN_GROUP, DEV_GROUP}, { MAIN_GROUP: parse_marker("platform_machine == 'amd64'"), DEV_GROUP: parse_marker("platform_machine == 'aarch64'"), }, ), }, ), ], parse_constraint("*"), ) assert len(packages) == 2 assert packages[a].groups == {"main", "dev"} assert tm(packages[a]) == { "main": 'python_version < "3.9" and sys_platform == "win32"', "dev": 'python_version >= "3.9" and sys_platform == "linux"', } assert packages[b].groups == {"main", "dev"} assert tm(packages[b]) == { "main": ( 'python_version < "3.9" and sys_platform == "win32"' ' or python_version >= "3.9" and platform_machine == "amd64"' ), "dev": ( 'python_version < "3.9" and sys_platform == "linux"' ' or python_version >= "3.9" and platform_machine == "aarch64"' ), } def test_merge_override_packages_shortcut(package: ProjectPackage) -> None: a = Package("a", "1") common_marker = ( 'extra == "test" and sys_platform == "win32" or platform_system == "Windows"' ' or sys_platform == "linux" and extra == "stretch"' ) override_marker1 = 'python_version >= "3.12" and platform_system != "Emscripten"' override_marker2 = 'python_version >= "3.12" and platform_system == "Emscripten"' packages = merge_override_packages( [ ( {package: {"a": dep("b", override_marker1)}}, { a: TransitivePackageInfo( 0, {MAIN_GROUP}, { MAIN_GROUP: parse_marker( f"{override_marker1} and ({common_marker})" ) }, ) }, ), ( {package: {"a": dep("b", override_marker2)}}, { a: TransitivePackageInfo( 0, {MAIN_GROUP}, { MAIN_GROUP: parse_marker( f"{override_marker2} and ({common_marker})" ) }, ) }, ), ], parse_constraint("*"), ) assert len(packages) == 1 assert packages[a].groups == {"main"} assert tm(packages[a]) == { "main": f'({common_marker}) and python_version >= "3.12"' } # TODO: root extras ================================================ FILE: tests/puzzle/test_transaction.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any import pytest from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import parse_marker from poetry.installation.operations.update import Update from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.puzzle.transaction import Transaction if TYPE_CHECKING: from poetry.installation.operations.operation import Operation DEV_GROUP = canonicalize_name("dev") def get_transitive_info(depth: int) -> TransitivePackageInfo: return TransitivePackageInfo(depth, set(), {}) def check_operations(ops: list[Operation], expected: list[dict[str, Any]]) -> None: for e in expected: if "skipped" not in e: e["skipped"] = False result = [] for op in ops: if op.job_type == "update": assert isinstance(op, Update) result.append( { "job": "update", "from": op.initial_package, "to": op.target_package, "skipped": op.skipped, } ) else: job = "install" if op.job_type == "uninstall": job = "remove" result.append({"job": job, "package": op.package, "skipped": op.skipped}) assert result == expected def test_it_should_calculate_operations_in_correct_order() -> None: transaction = Transaction( [Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")], { Package("a", "1.0.0"): get_transitive_info(1), Package("b", "2.1.0"): get_transitive_info(2), Package("d", "4.0.0"): get_transitive_info(0), }, ) check_operations( transaction.calculate_operations(), [ {"job": "install", "package": Package("b", "2.1.0")}, {"job": "install", "package": Package("a", "1.0.0")}, {"job": "install", "package": Package("d", "4.0.0")}, ], ) def test_it_should_calculate_operations_for_installed_packages() -> None: transaction = Transaction( [Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")], { Package("a", "1.0.0"): get_transitive_info(1), Package("b", "2.1.0"): get_transitive_info(2), Package("d", "4.0.0"): get_transitive_info(0), }, installed_packages=[ Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0"), Package("e", "5.0.0"), ], ) check_operations( transaction.calculate_operations(), [ {"job": "remove", "package": Package("c", "3.0.0")}, { "job": "update", "from": Package("b", "2.0.0"), "to": Package("b", "2.1.0"), }, {"job": "install", "package": Package("a", "1.0.0"), "skipped": True}, {"job": "install", "package": Package("d", "4.0.0")}, ], ) def test_it_should_remove_installed_packages_if_required() -> None: transaction = Transaction( [Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")], { Package("a", "1.0.0"): get_transitive_info(1), Package("b", "2.1.0"): get_transitive_info(2), Package("d", "4.0.0"): get_transitive_info(0), }, installed_packages=[ Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0"), Package("e", "5.0.0"), ], ) check_operations( transaction.calculate_operations(synchronize=True), [ {"job": "remove", "package": Package("c", "3.0.0")}, {"job": "remove", "package": Package("e", "5.0.0")}, { "job": "update", "from": Package("b", "2.0.0"), "to": Package("b", "2.1.0"), }, {"job": "install", "package": Package("a", "1.0.0"), "skipped": True}, {"job": "install", "package": Package("d", "4.0.0")}, ], ) def test_it_should_not_remove_system_site_packages() -> None: """ Different types of uninstalls: - c: tracked but not required - e: not tracked - f: root extra that is not requested """ extra_name = canonicalize_name("foo") package = ProjectPackage("root", "1.0") dep_f = Dependency("f", "1", optional=True) dep_f._in_extras = [extra_name] package.add_dependency(dep_f) package.extras = {extra_name: [dep_f]} opt_f = Package("f", "6.0.0") opt_f.optional = True transaction = Transaction( [Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")], { Package("a", "1.0.0"): get_transitive_info(1), Package("b", "2.1.0"): get_transitive_info(2), Package("d", "4.0.0"): get_transitive_info(0), opt_f: get_transitive_info(0), }, installed_packages=[ Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0"), Package("e", "5.0.0"), Package("f", "6.0.0"), ], root_package=package, ) check_operations( transaction.calculate_operations( synchronize=True, extras=set(), system_site_packages={ canonicalize_name(name) for name in ("a", "b", "c", "e", "f") }, ), [ { "job": "update", "from": Package("b", "2.0.0"), "to": Package("b", "2.1.0"), }, {"job": "install", "package": Package("a", "1.0.0"), "skipped": True}, {"job": "install", "package": Package("d", "4.0.0")}, ], ) def test_it_should_not_remove_installed_packages_that_are_in_result() -> None: transaction = Transaction( [], { Package("a", "1.0.0"): get_transitive_info(1), Package("b", "2.0.0"): get_transitive_info(2), Package("c", "3.0.0"): get_transitive_info(0), }, installed_packages=[ Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0"), ], ) check_operations( transaction.calculate_operations(synchronize=True), [ {"job": "install", "package": Package("a", "1.0.0"), "skipped": True}, {"job": "install", "package": Package("b", "2.0.0"), "skipped": True}, {"job": "install", "package": Package("c", "3.0.0"), "skipped": True}, ], ) def test_it_should_update_installed_packages_if_sources_are_different() -> None: transaction = Transaction( [Package("a", "1.0.0")], { Package( "a", "1.0.0", source_url="https://github.com/demo/demo.git", source_type="git", source_reference="main", source_resolved_reference="123456", ): get_transitive_info(1) }, installed_packages=[Package("a", "1.0.0")], ) check_operations( transaction.calculate_operations(synchronize=True), [ { "job": "update", "from": Package("a", "1.0.0"), "to": Package( "a", "1.0.0", source_url="https://github.com/demo/demo.git", source_type="git", source_reference="main", source_resolved_reference="123456", ), } ], ) @pytest.mark.parametrize( ("groups", "expected"), [ (set(), []), ({"main"}, ["a", "c"]), ({"dev"}, ["b", "c"]), ({"main", "dev"}, ["a", "b", "c"]), ], ) @pytest.mark.parametrize("installed", [False, True]) @pytest.mark.parametrize("with_uninstalls", [False, True]) @pytest.mark.parametrize("sync", [False, True]) def test_calculate_operations_with_groups( installed: bool, with_uninstalls: bool, sync: bool, groups: set[str], expected: list[str], ) -> None: transaction = Transaction( [Package("a", "1"), Package("b", "1"), Package("c", "1"), Package("d", "1")], { Package("a", "1"): TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: AnyMarker()} ), Package("b", "1"): TransitivePackageInfo( 0, {DEV_GROUP}, {DEV_GROUP: AnyMarker()} ), Package("c", "1"): TransitivePackageInfo( 0, {MAIN_GROUP, DEV_GROUP}, {MAIN_GROUP: AnyMarker(), DEV_GROUP: AnyMarker()}, ), }, ( [Package("a", "1"), Package("b", "1"), Package("c", "1"), Package("d", "1")] if installed else [] ), None, {"python_version": "3.8"}, {canonicalize_name(g) for g in groups}, ) expected_ops = [ {"job": "install", "package": Package(name, "1")} for name in expected ] if installed: for op in expected_ops: op["skipped"] = True if with_uninstalls: expected_ops.insert(0, {"job": "remove", "package": Package("d", "1")}) if sync: for name in sorted({"a", "b", "c"}.difference(expected), reverse=True): expected_ops.insert( 0, {"job": "remove", "package": Package(name, "1")} ) check_operations( transaction.calculate_operations( with_uninstalls=with_uninstalls, synchronize=sync ), expected_ops, ) @pytest.mark.parametrize( ("python_version", "expected"), [("3.8", ["a"]), ("3.9", ["b"])] ) @pytest.mark.parametrize("installed", [False, True]) @pytest.mark.parametrize("sync", [False, True]) def test_calculate_operations_with_markers( installed: bool, sync: bool, python_version: str, expected: list[str] ) -> None: transaction = Transaction( [Package("a", "1"), Package("b", "1")], { Package("a", "1"): TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("python_version < '3.9'")} ), Package("b", "1"): TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("python_version >= '3.9'")} ), }, [Package("a", "1"), Package("b", "1")] if installed else [], None, {"python_version": python_version}, {MAIN_GROUP}, ) expected_ops = [ {"job": "install", "package": Package(name, "1")} for name in expected ] if installed: for op in expected_ops: op["skipped"] = True if sync: for name in sorted({"a", "b"}.difference(expected), reverse=True): expected_ops.insert(0, {"job": "remove", "package": Package(name, "1")}) check_operations( transaction.calculate_operations(with_uninstalls=sync, synchronize=sync), expected_ops, ) @pytest.mark.parametrize( ("python_version", "sys_platform", "groups", "expected"), [ ("3.8", "win32", {"main"}, True), ("3.9", "linux", {"main"}, False), ("3.9", "linux", {"dev"}, True), ("3.8", "win32", {"dev"}, False), ("3.9", "linux", {"main", "dev"}, True), ("3.8", "win32", {"main", "dev"}, True), ("3.8", "linux", {"main", "dev"}, True), ("3.9", "win32", {"main", "dev"}, False), ], ) def test_calculate_operations_with_groups_and_markers( python_version: str, sys_platform: str, groups: set[str], expected: bool, ) -> None: transaction = Transaction( [Package("a", "1")], { Package("a", "1"): TransitivePackageInfo( 0, {MAIN_GROUP, DEV_GROUP}, { MAIN_GROUP: parse_marker("python_version < '3.9'"), DEV_GROUP: parse_marker("sys_platform == 'linux'"), }, ), }, [], None, {"python_version": python_version, "sys_platform": sys_platform}, {canonicalize_name(g) for g in groups}, ) expected_ops = ( [{"job": "install", "package": Package("a", "1")}] if expected else [] ) check_operations(transaction.calculate_operations(), expected_ops) @pytest.mark.parametrize("extras", [False, True]) @pytest.mark.parametrize("marker_env", [False, True]) @pytest.mark.parametrize("installed", [False, True]) @pytest.mark.parametrize("with_uninstalls", [False, True]) @pytest.mark.parametrize("sync", [False, True]) def test_calculate_operations_extras( extras: bool, marker_env: bool, installed: bool, with_uninstalls: bool, sync: bool, ) -> None: extra_name = canonicalize_name("foo") package = ProjectPackage("root", "1.0") dep_a = Dependency("a", "1", optional=True) dep_a._in_extras = [extra_name] package.add_dependency(dep_a) package.extras = {extra_name: [dep_a]} opt_a = Package("a", "1") opt_a.optional = True transaction = Transaction( [Package("a", "1")], { opt_a: TransitivePackageInfo( 0, {MAIN_GROUP} if marker_env else set(), {MAIN_GROUP: parse_marker("extra == 'foo'")} if marker_env else {}, ) }, [Package("a", "1")] if installed else [], package, {"python_version": "3.8"} if marker_env else None, {MAIN_GROUP} if marker_env else None, ) if extras: ops = [{"job": "install", "package": Package("a", "1"), "skipped": installed}] elif installed: if with_uninstalls and sync: ops = [{"job": "remove", "package": Package("a", "1")}] else: ops = [] else: ops = [{"job": "install", "package": Package("a", "1"), "skipped": True}] check_operations( transaction.calculate_operations( with_uninstalls=with_uninstalls, synchronize=sync, extras={extra_name} if extras else set(), ), ops, ) @pytest.mark.parametrize("extra", ["", "foo", "bar"]) def test_calculate_operations_extras_no_redundant_uninstall(extra: str) -> None: extra1 = canonicalize_name("foo") extra2 = canonicalize_name("bar") package = ProjectPackage("root", "1.0") dep_a1 = Dependency("a", "1", optional=True) dep_a1._in_extras = [canonicalize_name("foo")] dep_a1.marker = parse_marker("extra != 'bar'") dep_a2 = Dependency("a", "2", optional=True) dep_a2._in_extras = [canonicalize_name("bar")] dep_a2.marker = parse_marker("extra != 'foo'") package.add_dependency(dep_a1) package.add_dependency(dep_a2) package.extras = {extra1: [dep_a1], extra2: [dep_a2]} opt_a1 = Package("a", "1") opt_a1.optional = True opt_a2 = Package("a", "2") opt_a2.optional = True transaction = Transaction( [Package("a", "1")], { opt_a1: TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("extra == 'foo' and extra != 'bar'")}, ), opt_a2: TransitivePackageInfo( 0, {MAIN_GROUP}, {MAIN_GROUP: parse_marker("extra == 'bar' and extra != 'foo'")}, ), }, [Package("a", "1")], package, {"python_version": "3.9"}, {MAIN_GROUP}, ) if not extra: ops = [{"job": "remove", "package": Package("a", "1")}] elif extra == "foo": ops = [{"job": "install", "package": Package("a", "1"), "skipped": True}] elif extra == "bar": ops = [{"job": "update", "from": Package("a", "1"), "to": Package("a", "2")}] else: raise NotImplementedError check_operations( transaction.calculate_operations( synchronize=True, extras=set() if not extra else {canonicalize_name(extra)}, ), ops, ) ================================================ FILE: tests/pyproject/__init__.py ================================================ ================================================ FILE: tests/pyproject/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from pathlib import Path @pytest.fixture def pyproject_toml(tmp_path: Path) -> Path: path = tmp_path / "pyproject.toml" with path.open(mode="w", encoding="utf-8"): pass return path @pytest.fixture def build_system_section(pyproject_toml: Path) -> str: content = """ [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" """ with pyproject_toml.open(mode="a", encoding="utf-8") as f: f.write(content) return content @pytest.fixture def poetry_section(pyproject_toml: Path) -> str: content = """ [tool.poetry] name = "poetry" [tool.poetry.dependencies] python = "^3.5" """ with pyproject_toml.open(mode="a", encoding="utf-8") as f: f.write(content) return content ================================================ FILE: tests/pyproject/test_pyproject_toml.py ================================================ from __future__ import annotations import uuid from typing import TYPE_CHECKING from poetry.pyproject.toml import PyProjectTOML if TYPE_CHECKING: from pathlib import Path def test_pyproject_toml_reload(pyproject_toml: Path, poetry_section: str) -> None: pyproject = PyProjectTOML(pyproject_toml) name_original = pyproject.poetry_config["name"] name_new = str(uuid.uuid4()) pyproject.poetry_config["name"] = name_new assert isinstance(pyproject.poetry_config["name"], str) assert pyproject.poetry_config["name"] == name_new pyproject.reload() assert pyproject.poetry_config["name"] == name_original def test_pyproject_toml_save( pyproject_toml: Path, poetry_section: str, build_system_section: str ) -> None: pyproject = PyProjectTOML(pyproject_toml) name = str(uuid.uuid4()) build_backend = str(uuid.uuid4()) build_requires = str(uuid.uuid4()) pyproject.poetry_config["name"] = name pyproject.build_system.build_backend = build_backend pyproject.build_system.requires.append(build_requires) pyproject.save() pyproject = PyProjectTOML(pyproject_toml) assert isinstance(pyproject.poetry_config["name"], str) assert pyproject.poetry_config["name"] == name assert pyproject.build_system.build_backend == build_backend assert build_requires in pyproject.build_system.requires ================================================ FILE: tests/pyproject/test_pyproject_toml_file.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.core.exceptions import PoetryCoreError from poetry.toml import TOMLFile if TYPE_CHECKING: from pathlib import Path def test_pyproject_toml_file_invalid(pyproject_toml: Path) -> None: with pyproject_toml.open(mode="a", encoding="utf-8") as f: f.write("<<<<<<<<<<<") with pytest.raises(PoetryCoreError) as excval: _ = TOMLFile(pyproject_toml).read() assert f"Invalid TOML file {pyproject_toml.as_posix()}" in str(excval.value) ================================================ FILE: tests/repositories/__init__.py ================================================ ================================================ FILE: tests/repositories/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from tests.types import HTMLPageGetter @pytest.fixture def html_page_content() -> HTMLPageGetter: def _fixture(content: str, base_url: str | None = None) -> str: base = f' {base} Links for demo

Links for demo

{content} """ return _fixture ================================================ FILE: tests/repositories/fixtures/__init__.py ================================================ from __future__ import annotations pytest_plugins = [ "tests.repositories.fixtures.distribution_hashes", "tests.repositories.fixtures.legacy", "tests.repositories.fixtures.pypi", "tests.repositories.fixtures.python_hosted", ] ================================================ FILE: tests/repositories/fixtures/distribution_hashes.py ================================================ # this file is generated by tests/repositories/fixtures/pypi.org/generate.py from __future__ import annotations import dataclasses from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from tests.types import DistributionHashGetter @dataclasses.dataclass class DistributionHash: sha256: str = "" md5: str = "" KNOWN_DISTRIBUTION_HASHES = { "SQLAlchemy-1.2.12.tar.gz": DistributionHash( "b5a127599b3f27847fba6119de0fcb70832a8041b103701a708b7c7d044faa38", "4a2617b5254748828d09349fc4eff6bd", ), "Twisted-18.9.0.tar.bz2": DistributionHash( "4335327da58be11dd6e482ec6b85eb055bcc953a9570cd59e7840a2ce9419a8e", "35ff4705ea90a76bf972ff3b229546ca", ), "attrs-17.4.0-py2.py3-none-any.whl": DistributionHash( "1fbfc10ebc8c876dcbab17f016b80ae1a4f0c1413461a695871427960795beb4", "7fe37931797b16c7fa158017457a9ea9", ), "attrs-17.4.0.tar.gz": DistributionHash( "eb7536a1e6928190b3008c5b350bdf9850d619fff212341cd096f87a27a5e564", "c03e5b3608d9071fbd098850d8922668", ), "black-19.10b0-py36-none-any.whl": DistributionHash( "13001c5b7dbc81137164b43137320a1785e95ce84e4db849279786877ac6d7f6", "acc537b0f3f7ebf575616490d7cc14f4", ), "black-19.10b0.tar.gz": DistributionHash( "6cada614d5d2132698c6d5fff384657273d922c4fffa6a2f0de9e03e25b8913a", "c383543109a66a5a99113e6326db5251", ), "black-21.11b0-py3-none-any.whl": DistributionHash( "38f6ad54069912caf2fa2d4f25d0c5dedef4b2338a0cb545dbe2fdf54a6a8891", "92942a9efabf8e321a11360667ad2494", ), "black-21.11b0.tar.gz": DistributionHash( "f23c482185d842e2f19d506e55c004061167e3c677c063ecd721042c62086ada", "f01267bf2613f825dd6684629c1c829e", ), "cleo-1.0.0a5-py3-none-any.whl": DistributionHash( "d0cfea878b77be28be027033e6af419b705abe47278067a7c3a298f39cf825c5", "19ed7de77063e8f16bc459276ccbe197", ), "cleo-1.0.0a5.tar.gz": DistributionHash( "88f0a4275a17f2ab4d013786b8b9522d4c60bd37d8fc9b3def0fb27f4ac1e694", "92e181952976e09b9d1c583da6c3e2fc", ), "clikit-0.2.4-py2.py3-none-any.whl": DistributionHash( "27316bf6382b04be8fb2f60c85d538fd2b2b03f0f1eba5c88f7d7eddbefc2778", "93a51e8bf259c29692e51a7cbca6d664", ), "clikit-0.2.4.tar.gz": DistributionHash( "0fdd41e86e8b118a8b1e94ef2835925ada541d481c9b3b2fc635fa68713e6125", "f7cdbad3508038a04561f646aae68146", ), "colorama-0.3.9-py2.py3-none-any.whl": DistributionHash( "78a441d2e984c790526cdef1cfd8415a366979ef5b3186771a055b35886953bf", "8021c861015b5f590be41190bc3f8eed", ), "colorama-0.3.9.tar.gz": DistributionHash( "4c5a15209723ce1330a5c193465fe221098f761e9640d823a2ce7c03f983137f", "8323a5b84fdf7ad810804e51fc256b39", ), "demo-0.1.0-py2.py3-none-any.whl": DistributionHash( "70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", "15507846fd4299596661d0197bfb4f90", ), "demo-0.1.0.tar.gz": DistributionHash( "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", "d1912c917363a64e127318655f7d1fe7", ), "demo-0.1.2-py2.py3-none-any.whl": DistributionHash( "55dde4e6828081de7a1e429f33180459c333d9da593db62a3d75a8f5e505dde1", "53b4e10d2bfa81a4206221c4b87843d9", ), "demo_invalid_record-0.1.0-py2.py3-none-any.whl": DistributionHash( "d1e5a3f18f24a2ad3717c6f9c55f8c26060f39b2cddf28b18c355786728cb4dd", "18041168d415370d5019ec7e2b1ed0b5", ), "demo_invalid_record2-0.1.0-py2.py3-none-any.whl": DistributionHash( "e730fca385b52e77fc58d73812f0dc236fad489ef6026716d1a4317ab4738c3c", "a21ee67e833f50e9f0ecdfe1c0484b93", ), "demo_metadata_version_23-0.1.0-py2.py3-none-any.whl": DistributionHash( "7592aa158137726d9579e5d4347bd03a88f9fc82e11061303215feaaf000d32c", "434114a36f986671d132033e130f26b7", ), "demo_metadata_version_24-0.1.0-py2.py3-none-any.whl": DistributionHash( "f0d306c48d665e4a0051c660cc39f5ed7b7d51427050bfbca525e95d9fad2587", "c0cbc2e5f2736a487ff960a8c39defbe", ), "demo_metadata_version_299-0.1.0-py2.py3-none-any.whl": DistributionHash( "9678f9e59454a281bf7780661d719122766111dc9432ad20823ce6569d10edb2", "2eb53ee23408e65de909e20d9575afe3", ), "demo_metadata_version_unknown-0.1.0-py2.py3-none-any.whl": DistributionHash( "d716cd66546468ec3d4d40f4a4ecc813e3e4c661e155ecbc3a932f47d46d6e05", "749f823ff755a2f46bfb5ab25fdf9810", ), "demo_missing_dist_info-0.1.0-py2.py3-none-any.whl": DistributionHash( "cf8eaade81dd1db42f60c0e9c8610c1c12006baa9f7ad994b1c2bae92ea4b426", "da33c6088e72fbaaf873999606767353", ), "demo_no_pkg_info-0.1.0.tar.gz": DistributionHash( "f1e2a977c506dfb6b43495e2ffeee618b90029bac92fcb3038a53268197afa0c", "eeaf257d6b2c3b01def567751b21c1e8", ), "discord.py-2.0.0-py3-none-any.whl": DistributionHash( "25b9739ba456622655203a0925b354c0ba96ac6c740562e7c37791c2f6b594fb", "65394fc868632423cedb6be7259db970", ), "discord.py-2.0.0.tar.gz": DistributionHash( "b86fa9dd562684f7a52564e6dfe0216f6c172a009c0d86b8dea8bdd6ffa6b1f4", "6c0505a6032342b29f31f9979f37d277", ), "futures-3.2.0-py2-none-any.whl": DistributionHash( "41353b36198757a766cfc82dc9b60e88ecb28e543dd92473b2cc74fc7bf205af", "f81c5c27f3ba2efc008cc96363a81c5e", ), "futures-3.2.0.tar.gz": DistributionHash( "baf0d469c9e541b747986b7404cd63a5496955bd0c43a3cc068c449b09b7d4a4", "40eb168dab84e606df3fdb7e67fe27b7", ), "hbmqtt-0.9.6.tar.gz": DistributionHash( "379f1d9044997c69308ac2e01621c817b5394e1fbe0696e62538ae2dd0aa7e07", "b284e3118882f169aa618a856cd91c5f", ), "ipython-5.7.0-py2-none-any.whl": DistributionHash( "4608e3e0500fe8142659d149891400fc0b9fa250051814b569457ae4688943dc", "20da5e0b1f79dccb37f033a885d798d7", ), "ipython-5.7.0-py3-none-any.whl": DistributionHash( "4292c026552a77b2edc0543941516eddd6fe1a4b681a76ac40b3f585d2fca76f", "2844fa01618fe27ab99ad455d605b47d", ), "ipython-5.7.0.tar.gz": DistributionHash( "4e7fb265e0264498bd0d62c6261936a658bf3d38beb8a7b10cd2c6327c62ac2a", "01f2808ebe78ff2f28dc39be3aa635ca", ), "ipython-7.5.0-py3-none-any.whl": DistributionHash( "1b4c76bf1e8dd9067a4f5ab4695d4c5ad81c30d7d06f7592f4c069c389e37f37", "f40ea889fb7adf989760c5e7a38bd112", ), "ipython-7.5.0.tar.gz": DistributionHash( "cd2a17ac273fea8bf8953118a2d83bad94f592f0db3e83fff9129a1842e36dbe", "0e8c1d7c14f309f6cd2dfd4e48e75cb1", ), "isodate-0.7.0-py3-none-any.whl": DistributionHash( "04505f97eb100b66dff1239859e6e04ab913714c453d6ab9591adbf418285847", "1af9e3ee3f5669186356afd2dbe7ce81", ), "isodate-0.7.0.tar.gz": DistributionHash( "167c3615c0bd2e498c9bae7a1aba5863a17e52299aafd89f17a3a091187dca74", "5668b7b7120797f03330363000afc35a", ), "isort-4.3.4-py2-none-any.whl": DistributionHash( "383c39c10b5db83e8d150ac5b84d74bda96e3a1b06a30257f022dcbcd21f54b9", "42bccda292eca3c91eadf3eb781a224f", ), "isort-4.3.4-py3-none-any.whl": DistributionHash( "5668dce9fb48544c57ed626982e190c8ea99e3a612850453e9c3b193b9fa2edc", "6c3b582d7782633ec23917b00a97a2fe", ), "isort-4.3.4.tar.gz": DistributionHash( "234ad07e1e2780c27fa56364eefa734bee991b0d744337ef7e7ce3d5b1b59f39", "9244631852cf8bd8559f7ab78bf4ec78", ), "jupyter-1.0.0-py2.py3-none-any.whl": DistributionHash( "1de1f2be45629dd6f7f9558e2385ddf6901849699ef1044c52d171a9b520a420", "27f411f164e0878104d76d868127f76f", ), "jupyter-1.0.0.tar.gz": DistributionHash( "3ef1e86ba0556ea5922b846416a41acfd2625830d996c7d06d80c90bed1dc193", "78acaec88533ea6b6e761e7d086a1d04", ), "jupyter-1.0.0.zip": DistributionHash( "4a855b9717c3ea24fd8ca4fd91ab5995894aecc4d20e7f39c28786a2c1869fae", "7b7a957694a73ac0c19fe46c216c0ea0", ), "more-itertools-4.1.0.tar.gz": DistributionHash( "bab2dc6f4be8f9a4a72177842c5283e2dff57c167439a03e3d8d901e854f0f2e", "bf351a1050242ce3af7e475a4da1a26b", ), "more_itertools-4.1.0-py2-none-any.whl": DistributionHash( "0f461c2cd4ec16611396f9ee57f40433de3d59e95475d84c0c829cde02f746cd", "703e1e0922de1f11823da60af1488b7a", ), "more_itertools-4.1.0-py3-none-any.whl": DistributionHash( "580b6002d1f28feb5bcb8303278d59cf17dfbd19a63a5c2375112dae72c9bf98", "ae17a45d13e9dc319794c40fa739c38f", ), "pastel-0.1.0-py3-none-any.whl": DistributionHash( "754d192c088e256d52a3f825c3b9e14252d5adc70f53656453f6431e50a70b99", "cf7c53ab0a5d7e7c721425b24b486124", ), "pastel-0.1.0.tar.gz": DistributionHash( "22f14474c4120b37c54ac2173b49b0ac1de9283ca714be6eb3ea8b39296285a9", "43ea5f07660f630da18ae1827f5b4333", ), "pluggy-0.6.0-py2-none-any.whl": DistributionHash( "f5f767d398f18aa177976bf9c4d0c05d96487a7d8f07062251585803aaf56246", "095eed084713c9b2a9a01520485e20fb", ), "pluggy-0.6.0-py3-none-any.whl": DistributionHash( "d34798b80853ab688de1a3ca5b99ba4de91c459c19c76a555dc939979ae67eb0", "2b6dc266f54023dfb26726686ee6b227", ), "pluggy-0.6.0.tar.gz": DistributionHash( "a982e208d054867661d27c6d2a86b17ba05fbb6b1bdc01f42660732dd107f865", "ef8a88abcd501afd47cb22245fe4315a", ), "poetry_core-1.5.0-py3-none-any.whl": DistributionHash( "e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84", "be7589b4902793e66d7d979bd8581591", ), "poetry_core-1.5.0.tar.gz": DistributionHash( "0ae8d28caf5c12ec1714b16d2e7157ddd52397ea6bfdeba5a9432e449a0184da", "3f9b36a7a94cd235bfd5f05794828445", ), "poetry_core-2.0.1-py3-none-any.whl": DistributionHash( "a3c7009536522cda4eb0fb3805c9dc935b5537f8727dd01efb9c15e51a17552b", "a52cf4beef0de009e0a9a36c9e6962f5", ), "poetry_core-2.0.1.tar.gz": DistributionHash( "d2acdaec3b93dc1ab43adaeb0e9a8a6a6b3701c4535b5baab4b718ab12c8993c", "1b1bb959cd760ac509de9b38ae67fc3b", ), "py-1.5.3-py2.py3-none-any.whl": DistributionHash( "ef4a94f47156178e42ef8f2b131db420e0f4b6aa0b3936b6dbde6ad6487476a5", "b316b380701661cb67732ecdaef30eeb", ), "py-1.5.3.tar.gz": DistributionHash( "2df2c513c3af11de15f58189ba5539ddc4768c6f33816dc5c03950c8bd6180fa", "623e80cfc06df930414a9ce4bf0fd6c9", ), "pytest-3.5.0-py2.py3-none-any.whl": DistributionHash( "427b4582bda18e92ad1967e8b1e071e2c53e6cb7e3e5f090cb3ca443455be23f", "4a8651dec151e76f283bf59e333286f9", ), "pytest-3.5.0.tar.gz": DistributionHash( "677b1d6decd29c041fe64276f29f79fbe66e40c59e445eb251366b4a8ab8bf68", "ccd78dac54112045f561c4df86631f19", ), "pytest-3.5.1-py2.py3-none-any.whl": DistributionHash( "d327df3686046c5b374a9776d9e11606f7dba6fb3db5cf5d60ebc78a31e0768e", "1e81fba94885bef80170545d045924eb", ), "pytest-3.5.1.tar.gz": DistributionHash( "b8fe151f3e181801dd38583a1c03818fbc662a8fce96c9063a0af624613e78f8", "961104636090457187851ccb9ef0f677", ), "python-language-server-0.21.2.tar.gz": DistributionHash( "91b564e092f3135b2bac70dbd23d283da5ad50269766a76648787b69fe702c7e", "677602ec38bc1c7b72de6128d90d846b", ), "requests-2.18.4-py2.py3-none-any.whl": DistributionHash( "098be851f30be5bcb2c7537798d44314f576e53818ba9def25141ae4dce8b25d", "e770e65750c42f40b97b0ed738d0f859", ), "requests-2.18.4.tar.gz": DistributionHash( "ec62f7e0e9d4814656b0172dbd592fea06127c6556ff5651eb5d2c8768671fd4", "942a6a383dc94da90cf58f5adcf028a4", ), "setuptools-67.6.1-py3-none-any.whl": DistributionHash( "e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078", "3b5b846e000da033d54eeaaf7915126e", ), "setuptools-67.6.1.tar.gz": DistributionHash( "a737d365c957dd3fced9ddd246118e95dce7a62c3dc49f37e7fdd9e93475d785", "ee2562f783544d1f95022c906dd3cf98", ), "six-1.11.0-py2.py3-none-any.whl": DistributionHash( "534e9875e44a507adec601c29b3cbd2ca6dae7df92bf3dd20c7289b2f99f7466", "9500094701f7201ddd065c60abcefef1", ), "six-1.11.0.tar.gz": DistributionHash( "268a4ccb159c1a2d2c79336b02e75058387b0cdbb4cea2f07846a758f48a356d", "25d3568604f921dd23532b88a0ce17e7", ), "tomlkit-0.5.2-py2.py3-none-any.whl": DistributionHash( "dea8ff39e9e2170f1b2f465520482eec71e7909cfff53dcb076b585d50f8ccc8", "4045c5f6848fbc93c38df2296a441f07", ), "tomlkit-0.5.2.tar.gz": DistributionHash( "4a226ccf11ee5a2e76bfc185747b54ee7718706aeb3aabb981327249dbe2b1d4", "7c31987ef6fba2cd64715cae27fade64", ), "tomlkit-0.5.3-py2.py3-none-any.whl": DistributionHash( "35f06da5835e85f149a4701d43e730adcc09f1b362e5fc2300d77bdd26280908", "3a90c70a5067d5727110838094ab8674", ), "tomlkit-0.5.3.tar.gz": DistributionHash( "e2f785651609492c771d9887ccb2369d891d16595d2d97972e2cbe5e8fb3439f", "cdbdc302a184d1f1e38d5e0810e3b212", ), "wheel-0.40.0-py3-none-any.whl": DistributionHash( "d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247", "517d39f133bd7b1ff17caf09784b7543", ), "wheel-0.40.0.tar.gz": DistributionHash( "5cb7e75751aa82e1b7db3fd52f5a9d59e7b06905630bed135793295931528740", "5f175a8d693f74878964d4fd29729ab7", ), "zipp-3.5.0-py3-none-any.whl": DistributionHash( "ec508cd5a3ed3d126293cafb34611469f2aef7342f575c3b6e072b995dc9da1f", "da62cbd850ba32ba93817aab0f03a855", ), "zipp-3.5.0.tar.gz": DistributionHash( "239d50954a15aa4b283023f18dc451ba811fb4d263f4dd6855642e4d1c80cc9f", "16bf2a24fae340052e8565c264d21092", ), } @pytest.fixture def dist_hash_getter() -> DistributionHashGetter: def get_hash(name: str) -> DistributionHash: return KNOWN_DISTRIBUTION_HASHES.get(name, DistributionHash()) return get_hash ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/cleo-0.7.6.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: cleo Version: 0.7.6 Summary: Cleo allows you to create beautiful and testable command-line interfaces. Home-page: https://github.com/sdispater/cleo License: MIT Keywords: cli,commands Author: Sébastien Eustace Author-email: sebastien@eustace.io Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Requires-Dist: clikit (>=0.4.0,<0.5.0) Description-Content-Type: text/x-rst Cleo #### .. image:: https://travis-ci.org/sdispater/cleo.png :alt: Cleo Build status :target: https://travis-ci.org/sdispater/cleo Create beautiful and testable command-line interfaces. Cleo is mostly a higher level wrapper for `CliKit `_, so a lot of the components and utilities comes from it. Refer to its documentation for more information. Resources ========= * `Documentation `_ * `Issue Tracker `_ Usage ===== To make a command that greets you from the command line, create ``greet_command.py`` and add the following to it: .. code-block:: python from cleo import Command class GreetCommand(Command): """ Greets someone greet {name? : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} """ def handle(self): name = self.argument('name') if name: text = 'Hello {}'.format(name) else: text = 'Hello' if self.option('yell'): text = text.upper() self.line(text) You also need to create the file to run at the command line which creates an ``Application`` and adds commands to it: .. code-block:: python #!/usr/bin/env python from greet_command import GreetCommand from cleo import Application application = Application() application.add(GreetCommand()) if __name__ == '__main__': application.run() Test the new command by running the following .. code-block:: bash $ python application.py greet John This will print the following to the command line: .. code-block:: text Hello John You can also use the ``--yell`` option to make everything uppercase: .. code-block:: bash $ python application.py greet John --yell This prints: .. code-block:: text HELLO JOHN As you may have already seen, Cleo uses the command docstring to determine the command definition. The docstring must be in the following form : .. code-block:: python """ Command description Command signature """ The signature being in the following form: .. code-block:: python """ command:name {argument : Argument description} {--option : Option description} """ The signature can span multiple lines. .. code-block:: python """ command:name {argument : Argument description} {--option : Option description} """ Coloring the Output ------------------- Whenever you output text, you can surround the text with tags to color its output. For example: .. code-block:: python # green text self.line('foo') # yellow text self.line('foo') # black text on a cyan background self.line('foo') # white text on a red background self.line('foo') The closing tag can be replaced by ````, which revokes all formatting options established by the last opened tag. It is possible to define your own styles using the ``add_style()`` method: .. code-block:: python self.add_style('fire', fg='red', bg='yellow', options=['bold', 'blink']) self.line('foo') Available foreground and background colors are: ``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan`` and ``white``. And available options are: ``bold``, ``underscore``, ``blink``, ``reverse`` and ``conceal``. You can also set these colors and options inside the tag name: .. code-block:: python # green text self.line('foo') # black text on a cyan background self.line('foo') # bold text on a yellow background self.line('foo') Verbosity Levels ---------------- Cleo has four verbosity levels. These are defined in the ``Output`` class: ======================================= ================================== ====================== Mode Meaning Console option ======================================= ================================== ====================== ``NA`` Do not output any messages ``-q`` or ``--quiet`` ``clikit.VERBOSITY_NORMAL`` The default verbosity level (none) ``clikit.VERBOSITY_VERBOSE`` Increased verbosity of messages ``-v`` ``clikit.VERBOSITY_VERY_VERBOSE`` Informative non essential messages ``-vv`` ``clikit.VERBOSITY_DEBUG`` Debug messages ``-vvv`` ======================================= ================================== ====================== It is possible to print a message in a command for only a specific verbosity level. For example: .. code-block:: python if clikit.VERBOSITY_VERBOSE <= self.io.verbosity: self.line(...) There are also more semantic methods you can use to test for each of the verbosity levels: .. code-block:: python if self.output.is_quiet(): # ... if self.output.is_verbose(): # ... You can also pass the verbosity flag directly to `line()`. .. code-block:: python self.line("", verbosity=clikit.VERBOSITY_VERBOSE) When the quiet level is used, all output is suppressed. Using Arguments --------------- The most interesting part of the commands are the arguments and options that you can make available. Arguments are the strings - separated by spaces - that come after the command name itself. They are ordered, and can be optional or required. For example, add an optional ``last_name`` argument to the command and make the ``name`` argument required: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {name : Who do you want to greet?} {last_name? : Your last name?} {--y|yell : If set, the task will yell in uppercase letters} """ You now have access to a ``last_name`` argument in your command: .. code-block:: python last_name = self.argument('last_name') if last_name: text += ' {}'.format(last_name) The command can now be used in either of the following ways: .. code-block:: bash $ python application.py greet John $ python application.py greet John Doe It is also possible to let an argument take a list of values (imagine you want to greet all your friends). For this it must be specified at the end of the argument list: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {names* : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} """ To use this, just specify as many names as you want: .. code-block:: bash $ python application.py demo:greet John Jane You can access the ``names`` argument as a list: .. code-block:: python names = self.argument('names') if names: text += ' {}'.format(', '.join(names)) There are 3 argument variants you can use: ================================ ==================================== =============================================================================================================== Mode Notation Value ================================ ==================================== =============================================================================================================== ``clikit.ARGUMENT_REQUIRED`` none (just write the argument name) The argument is required ``clikit.ARGUMENT_OPTIONAL`` ``argument?`` The argument is optional and therefore can be omitted ``clikit.ARGUMENT_MULTI_VALUED`` ``argument*`` The argument can contain an indefinite number of arguments and must be used at the end of the argument list ================================ ==================================== =============================================================================================================== You can combine them like this: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {names?* : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} """ If you want to set a default value, you can it like so: .. code-block:: text argument=default The argument will then be considered optional. Using Options ------------- Unlike arguments, options are not ordered (meaning you can specify them in any order) and are specified with two dashes (e.g. ``--yell`` - you can also declare a one-letter shortcut that you can call with a single dash like ``-y``). Options are *always* optional, and can be setup to accept a value (e.g. ``--dir=src``) or simply as a boolean flag without a value (e.g. ``--yell``). .. tip:: It is also possible to make an option *optionally* accept a value (so that ``--yell`` or ``--yell=loud`` work). Options can also be configured to accept a list of values. For example, add a new option to the command that can be used to specify how many times in a row the message should be printed: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {name? : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} {--iterations=1 : How many times should the message be printed?} """ Next, use this in the command to print the message multiple times: .. code-block:: python for _ in range(0, self.option('iterations')): self.line(text) Now, when you run the task, you can optionally specify a ``--iterations`` flag: .. code-block:: bash $ python application.py demo:greet John $ python application.py demo:greet John --iterations=5 The first example will only print once, since ``iterations`` is empty and defaults to ``1``. The second example will print five times. Recall that options don't care about their order. So, either of the following will work: .. code-block:: bash $ python application.py demo:greet John --iterations=5 --yell $ python application.py demo:greet John --yell --iterations=5 There are 4 option variants you can use: ================================ =================================== ====================================================================================== Option Notation Value ================================ =================================== ====================================================================================== ``clikit.OPTION_MULTI_VALUED`` ``--option=*`` This option accepts multiple values (e.g. ``--dir=/foo --dir=/bar``) ``clikit.OPTION_NO_VALUE`` ``--option`` Do not accept input for this option (e.g. ``--yell``) ``clikit.OPTION_REQUIRED_VALUE`` ``--option=`` This value is required (e.g. ``--iterations=5``), the option itself is still optional ``clikit.OPTION_OPTIONAL_VALUE`` ``--option=?`` This option may or may not have a value (e.g. ``--yell`` or ``--yell=loud``) ================================ =================================== ====================================================================================== You can combine them like this: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {name? : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} {--iterations=?*1 : How many times should the message be printed?} """ Testing Commands ---------------- Cleo provides several tools to help you test your commands. The most useful one is the ``CommandTester`` class. It uses a special IO class to ease testing without a real console: .. code-block:: python import pytest from cleo import Application from cleo import CommandTester def test_execute(self): application = Application() application.add(GreetCommand()) command = application.find('demo:greet') command_tester = CommandTester(command) command_tester.execute() assert "..." == tester.io.fetch_output() The ``CommandTester.io.fetch_output()`` method returns what would have been displayed during a normal call from the console. ``CommandTester.io.fetch_error()`` is also available to get what you have been written to the stderr. You can test sending arguments and options to the command by passing them as a string to the ``CommandTester.execute()`` method: .. code-block:: python import pytest from cleo import Application from cleo import CommandTester def test_execute(self): application = Application() application.add(GreetCommand()) command = application.find('demo:greet') command_tester = CommandTester(command) command_tester.execute("John") assert "John" in tester.io.fetch_output() You can also test a whole console application by using the ``ApplicationTester`` class. Calling an existing Command --------------------------- If a command depends on another one being run before it, instead of asking the user to remember the order of execution, you can call it directly yourself. This is also useful if you want to create a "meta" command that just runs a bunch of other commands. Calling a command from another one is straightforward: .. code-block:: python def handle(self): return_code = self.call('demo:greet', "John --yell") # ... If you want to suppress the output of the executed command, you can use the ``call_silent()`` method instead. Autocompletion -------------- Cleo supports automatic (tab) completion in ``bash``, ``zsh`` and ``fish``. To activate support for autocompletion, pass a ``complete`` keyword when initializing your application: .. code-block:: python application = Application('My Application', '0.1', complete=True) Now, register completion for your application by running one of the following in a terminal, replacing ``[program]`` with the command you use to run your application: .. code-block:: bash # BASH - Ubuntu / Debian [program] completions bash | sudo tee /etc/bash_completion.d/[program].bash-completion # BASH - Mac OSX (with Homebrew "bash-completion") [program] completions bash > $(brew --prefix)/etc/bash_completion.d/[program].bash-completion # ZSH - Config file mkdir ~/.zfunc echo "fpath+=~/.zfunc" >> ~/.zshrc [program] completions zsh > ~/.zfunc/_test # FISH [program] completions fish > ~/.config/fish/completions/[program].fish # FISH - Mac OSX (with Homebrew "fish") [program] completions fish > $(brew --prefix)/share/fish/vendor_completions.d/[program].fish ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/directory_pep_610-1.2.3.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: directory-pep-610 Version: 1.2.3 Summary: Foo License: MIT Requires-Python: >=3.6 ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/directory_pep_610-1.2.3.dist-info/direct_url.json ================================================ { "url": "file:///path/to/distributions/directory-pep-610", "dir_info": {} } ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-2.3.4.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: editable Version: 2.3.4 Summary: Editable description. License: MIT Keywords: cli,commands Author: Foo Bar Author-email: foo@bar.com Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Description-Content-Type: text/x-rst Editable #### ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir-2.3.4.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: editable-src-dir Version: 2.3.4 Summary: Editable description. License: MIT Keywords: cli,commands Author: Foo Bar Author-email: foo@bar.com Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Description-Content-Type: text/x-rst Editable #### ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir.pth ================================================ /path/to/editable/src ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import-2.3.4.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: editable-with-import Version: 2.3.4 Summary: Editable description. License: MIT Keywords: cli,commands Author: Foo Bar Author-email: foo@bar.com Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Description-Content-Type: text/x-rst Editable #### ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import.pth ================================================ import os ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable.pth ================================================ /path/to/editable ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable_directory_pep_610-1.2.3.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: editable-directory-pep-610 Version: 1.2.3 Summary: Foo License: MIT Requires-Python: >=3.6 ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable_directory_pep_610-1.2.3.dist-info/direct_url.json ================================================ { "url": "file:///path/to/distributions/directory-pep-610", "dir_info": { "editable": true } } ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/file_pep_610-1.2.3.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: file-pep-610 Version: 1.2.3 Summary: Foo License: MIT Requires-Python: >=3.6 ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/file_pep_610-1.2.3.dist-info/direct_url.json ================================================ { "url": "file:///path/to/distributions/file-pep-610-1.2.3.tar.gz", "archive_info": { "hash": "sha256=2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" } } ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610-1.2.3.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: git-pep-610 Version: 1.2.3 Summary: Foo License: MIT Requires-Python: >=3.6 ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610-1.2.3.dist-info/direct_url.json ================================================ { "url": "https://github.com/demo/git-pep-610.git", "vcs_info": { "vcs": "git", "requested_revision": "my-branch", "commit_id": "123456" } } ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_no_requested_version-1.2.3.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: git-pep-610-no-requested-version Version: 1.2.3 Summary: Foo License: MIT Requires-Python: >=3.6 ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_no_requested_version-1.2.3.dist-info/direct_url.json ================================================ { "url": "https://github.com/demo/git-pep-610-no-requested-version.git", "vcs_info": { "vcs": "git", "commit_id": "123456" } } ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_subdirectory-1.2.3.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: git-pep-610-subdirectory Version: 1.2.3 Summary: Foo License: MIT Requires-Python: >=3.6 ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610_subdirectory-1.2.3.dist-info/direct_url.json ================================================ { "url": "https://github.com/demo/git-pep-610-subdirectory.git", "vcs_info": { "vcs": "git", "requested_revision": "my-branch", "commit_id": "123456" }, "subdirectory": "subdir" } ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/standard-1.2.3.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: standard Version: 1.2.3 Summary: Standard description. License: MIT Keywords: cli,commands Author: Foo Bar Author-email: foo@bar.com Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Description-Content-Type: text/x-rst Editable #### ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/standard.pth ================================================ standard ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/url_pep_610-1.2.3.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: url-pep-610 Version: 1.2.3 Summary: Foo License: MIT Requires-Python: >=3.6 ================================================ FILE: tests/repositories/fixtures/installed/lib/python3.7/site-packages/url_pep_610-1.2.3.dist-info/direct_url.json ================================================ { "url": "https://mock.pythonhosted.org/distributions/url-pep-610-1.2.3.tar.gz", "archive_info": {} } ================================================ FILE: tests/repositories/fixtures/installed/lib64/python3.7/site-packages/bender-2.0.5.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: bender Version: 2.0.5 Summary: Python datetimes made easy License: MIT Keywords: cli,commands Author: Leela Author-email: leela@planetexpress.com Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Description-Content-Type: text/x-rst ================================================ FILE: tests/repositories/fixtures/installed/lib64/python3.7/site-packages/bender.pth ================================================ ../../../src/bender ================================================ FILE: tests/repositories/fixtures/installed/lib64/python3.7/site-packages/lib64-2.3.4.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: lib64 Version: 2.3.4 Summary: lib64 description. License: MIT Keywords: cli,commands Author: Foo Bar Author-email: foo@bar.com Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Description-Content-Type: text/x-rst lib64 #### ================================================ FILE: tests/repositories/fixtures/installed/src/bender/bender.egg-info/PKG-INFO ================================================ Metadata-Version: 2.1 Name: bender Version: 2.0.5 Summary: Python datetimes made easy License: MIT Keywords: cli,commands Author: Leela Author-email: leela@planetexpress.com Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Description-Content-Type: text/x-rst ================================================ FILE: tests/repositories/fixtures/installed/src/pendulum/pendulum.egg-info/PKG-INFO ================================================ Metadata-Version: 1.2 Name: pendulum Version: 2.0.5 Summary: Python datetimes made easy Home-page: https://pendulum.eustace.io Author: Sébastien Eustace Author-email: sebastien@eustace.io License: UNKNOWN Description: Pendulum ######## .. image:: https://img.shields.io/pypi/v/pendulum.svg :target: https://pypi.python.org/pypi/pendulum .. image:: https://img.shields.io/pypi/l/pendulum.svg :target: https://pypi.python.org/pypi/pendulum .. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg :target: https://codecov.io/gh/sdispater/pendulum/branch/master .. image:: https://travis-ci.org/sdispater/pendulum.svg :alt: Pendulum Build status :target: https://travis-ci.org/sdispater/pendulum Python datetimes made easy. Supports Python **2.7** and **3.4+**. .. code-block:: python >>> import pendulum >>> now_in_paris = pendulum.now('Europe/Paris') >>> now_in_paris '2016-07-04T00:49:58.502116+02:00' # Seamless timezone switching >>> now_in_paris.in_timezone('UTC') '2016-07-03T22:49:58.502116+00:00' >>> tomorrow = pendulum.now().add(days=1) >>> last_week = pendulum.now().subtract(weeks=1) >>> past = pendulum.now().subtract(minutes=2) >>> past.diff_for_humans() >>> '2 minutes ago' >>> delta = past - last_week >>> delta.hours 23 >>> delta.in_words(locale='en') '6 days 23 hours 58 minutes' # Proper handling of datetime normalization >>> pendulum.datetime(2013, 3, 31, 2, 30, tz='Europe/Paris') '2013-03-31T03:30:00+02:00' # 2:30 does not exist (Skipped time) # Proper handling of dst transitions >>> just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz='Europe/Paris') '2013-03-31T01:59:59.999999+01:00' >>> just_before.add(microseconds=1) '2013-03-31T03:00:00+02:00' Why Pendulum? ============= Native ``datetime`` instances are enough for basic cases but when you face more complex use-cases they often show limitations and are not so intuitive to work with. ``Pendulum`` provides a cleaner and more easy to use API while still relying on the standard library. So it's still ``datetime`` but better. Unlike other datetime libraries for Python, Pendulum is a drop-in replacement for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime`` instances by ``DateTime`` instances in you code (exceptions exist for libraries that check the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance). It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware and by default in ``UTC`` for ease of use. Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties. Why not Arrow? ============== Arrow is the most popular datetime library for Python right now, however its behavior and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything and it will try its best to return something while silently failing to handle some cases: .. code-block:: python arrow.get('2016-1-17') # pendulum.parse('2016-1-17') # arrow.get('20160413') # pendulum.parse('20160413') # arrow.get('2016-W07-5') # pendulum.parse('2016-W07-5') # # Working with DST just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris') just_after = just_before.replace(microseconds=1) '2013-03-31T02:00:00+02:00' # Should be 2013-03-31T03:00:00+02:00 (just_after.to('utc') - just_before.to('utc')).total_seconds() -3599.999999 # Should be 1e-06 just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris') just_after = just_before.add(microseconds=1) '2013-03-31T03:00:00+02:00' (just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds() 1e-06 Those are a few examples showing that Arrow cannot always be trusted to have a consistent behavior with the data you are passing to it. Limitations =========== Even though the ``DateTime`` class is a subclass of ``datetime`` there are some rare cases where it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with a possible solution, if any: * ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter: .. code-block:: python from pendulum import DateTime from sqlite3 import register_adapter register_adapter(DateTime, lambda val: val.isoformat(' ')) * ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter: .. code-block:: python import MySQLdb.converters import pymysql.converters from pendulum import DateTime MySQLdb.converters.conversions[DateTime] = MySQLdb.converters.DateTime2literal pymysql.converters.conversions[DateTime] = pymysql.converters.escape_datetime * ``django`` will use the ``isoformat()`` method to store datetimes in the database. However since ``pendulum`` is always timezone aware the offset information will always be returned by ``isoformat()`` raising an error, at least for MySQL databases. To work around it you can either create your own ``DateTimeField`` or use the previous workaround for ``MySQLdb``: .. code-block:: python from django.db.models import DateTimeField as BaseDateTimeField from pendulum import DateTime class DateTimeField(BaseDateTimeField): def value_to_string(self, obj): val = self.value_from_object(obj) if isinstance(value, DateTime): return value.to_datetime_string() return '' if val is None else val.isoformat() Resources ========= * `Official Website `_ * `Documentation `_ * `Issue Tracker `_ Contributing ============ Contributions are welcome, especially with localization. Getting started --------------- To work on the Pendulum codebase, you'll want to clone the project locally and install the required depedendencies via `poetry `_. .. code-block:: bash $ git clone git@github.com:sdispater/pendulum.git $ poetry install Localization ------------ If you want to help with localization, there are two different cases: the locale already exists or not. If the locale does not exist you will need to create it by using the ``clock`` utility: .. code-block:: bash ./clock locale create It will generate a directory in ``pendulum/locales`` named after your locale, with the following structure: .. code-block:: text / - custom.py - locale.py The ``locale.py`` file must not be modified. It contains the translations provided by the CLDR database. The ``custom.py`` file is the one you want to modify. It contains the data needed by Pendulum that are not provided by the CLDR database. You can take the `en `_ data as a reference to see which data is needed. You should also add tests for the created or modified locale. Platform: UNKNOWN Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* ================================================ FILE: tests/repositories/fixtures/installed/src/pendulum/pendulum.egg-info/requires.txt ================================================ python-dateutil<3.0,>=2.6 pytzdata>=2018.3 [:python_version < "3.5"] typing<4.0,>=3.6 ================================================ FILE: tests/repositories/fixtures/installed/vendor/py3.7/attrs-19.3.0.dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: attrs Version: 19.3.0 Summary: Classes Without Boilerplate Home-page: https://www.attrs.org/ Author: Hynek Schlawack Author-email: hs@ox.cx Maintainer: Hynek Schlawack Maintainer-email: hs@ox.cx License: MIT Project-URL: Documentation, https://www.attrs.org/ Project-URL: Bug Tracker, https://github.com/python-attrs/attrs/issues Project-URL: Source Code, https://github.com/python-attrs/attrs Keywords: class,attribute,boilerplate Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Description-Content-Type: text/x-rst Provides-Extra: azure-pipelines Requires-Dist: coverage ; extra == 'azure-pipelines' Requires-Dist: hypothesis ; extra == 'azure-pipelines' Requires-Dist: pympler ; extra == 'azure-pipelines' Requires-Dist: pytest (>=4.3.0) ; extra == 'azure-pipelines' Requires-Dist: six ; extra == 'azure-pipelines' Requires-Dist: zope.interface ; extra == 'azure-pipelines' Requires-Dist: pytest-azurepipelines ; extra == 'azure-pipelines' Provides-Extra: dev Requires-Dist: coverage ; extra == 'dev' Requires-Dist: hypothesis ; extra == 'dev' Requires-Dist: pympler ; extra == 'dev' Requires-Dist: pytest (>=4.3.0) ; extra == 'dev' Requires-Dist: six ; extra == 'dev' Requires-Dist: zope.interface ; extra == 'dev' Requires-Dist: sphinx ; extra == 'dev' Requires-Dist: pre-commit ; extra == 'dev' Provides-Extra: docs Requires-Dist: sphinx ; extra == 'docs' Requires-Dist: zope.interface ; extra == 'docs' Provides-Extra: tests Requires-Dist: coverage ; extra == 'tests' Requires-Dist: hypothesis ; extra == 'tests' Requires-Dist: pympler ; extra == 'tests' Requires-Dist: pytest (>=4.3.0) ; extra == 'tests' Requires-Dist: six ; extra == 'tests' Requires-Dist: zope.interface ; extra == 'tests' .. image:: https://www.attrs.org/en/latest/_static/attrs_logo.png :alt: attrs Logo ====================================== ``attrs``: Classes Without Boilerplate ====================================== .. image:: https://readthedocs.org/projects/attrs/badge/?version=stable :target: https://www.attrs.org/en/stable/?badge=stable :alt: Documentation Status .. image:: https://attrs.visualstudio.com/attrs/_apis/build/status/python-attrs.attrs?branchName=master :target: https://attrs.visualstudio.com/attrs/_build/latest?definitionId=1&branchName=master :alt: CI Status .. image:: https://codecov.io/github/python-attrs/attrs/branch/master/graph/badge.svg :target: https://codecov.io/github/python-attrs/attrs :alt: Test Coverage .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: black .. teaser-begin ``attrs`` is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka `dunder `_ methods). Its main goal is to help you to write **concise** and **correct** software without slowing down your code. .. -spiel-end- For that, it gives you a class decorator and a way to declaratively define the attributes on that class: .. -code-begin- .. code-block:: pycon >>> import attr >>> @attr.s ... class SomeClass(object): ... a_number = attr.ib(default=42) ... list_of_numbers = attr.ib(factory=list) ... ... def hard_math(self, another_number): ... return self.a_number + sum(self.list_of_numbers) * another_number >>> sc = SomeClass(1, [1, 2, 3]) >>> sc SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) >>> sc.hard_math(3) 19 >>> sc == SomeClass(1, [1, 2, 3]) True >>> sc != SomeClass(2, [3, 2, 1]) True >>> attr.asdict(sc) {'a_number': 1, 'list_of_numbers': [1, 2, 3]} >>> SomeClass() SomeClass(a_number=42, list_of_numbers=[]) >>> C = attr.make_class("C", ["a", "b"]) >>> C("foo", "bar") C(a='foo', b='bar') After *declaring* your attributes ``attrs`` gives you: - a concise and explicit overview of the class's attributes, - a nice human-readable ``__repr__``, - a complete set of comparison methods (equality and ordering), - an initializer, - and much more, *without* writing dull boilerplate code again and again and *without* runtime performance penalties. On Python 3.6 and later, you can often even drop the calls to ``attr.ib()`` by using `type annotations `_. This gives you the power to use actual classes with actual types in your code instead of confusing ``tuple``\ s or `confusingly behaving `_ ``namedtuple``\ s. Which in turn encourages you to write *small classes* that do `one thing well `_. Never again violate the `single responsibility principle `_ just because implementing ``__init__`` et al is a painful drag. .. -testimonials- Testimonials ============ **Amber Hawkie Brown**, Twisted Release Manager and Computer Owl: Writing a fully-functional class using attrs takes me less time than writing this testimonial. **Glyph Lefkowitz**, creator of `Twisted `_, `Automat `_, and other open source software, in `The One Python Library Everyone Needs `_: I’m looking forward to is being able to program in Python-with-attrs everywhere. It exerts a subtle, but positive, design influence in all the codebases I’ve see it used in. **Kenneth Reitz**, creator of `Requests `_ (`on paper no less `_!): attrs—classes for humans. I like it. **Łukasz Langa**, creator of `Black `_, prolific Python core developer, and release manager for Python 3.8 and 3.9: I'm increasingly digging your attr.ocity. Good job! .. -end- .. -project-information- Getting Help ============ Please use the ``python-attrs`` tag on `StackOverflow `_ to get help. Answering questions of your fellow developers is also great way to help the project! Project Information =================== ``attrs`` is released under the `MIT `_ license, its documentation lives at `Read the Docs `_, the code on `GitHub `_, and the latest release on `PyPI `_. It’s rigorously tested on Python 2.7, 3.4+, and PyPy. We collect information on **third-party extensions** in our `wiki `_. Feel free to browse and add your own! If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide `_ to get you started! Release Information =================== 19.3.0 (2019-10-15) ------------------- Changes ^^^^^^^ - Fixed ``auto_attribs`` usage when default values cannot be compared directly with ``==``, such as ``numpy`` arrays. `#585 `_ `Full changelog `_. Credits ======= ``attrs`` is written and maintained by `Hynek Schlawack `_. The development is kindly supported by `Variomedia AG `_. A full list of contributors can be found in `GitHub's overview `_. It’s the spiritual successor of `characteristic `_ and aspires to fix some of it clunkiness and unfortunate decisions. Both were inspired by Twisted’s `FancyEqMixin `_ but both are implemented using class decorators because `subclassing is bad for you `_, m’kay? ================================================ FILE: tests/repositories/fixtures/legacy/absolute.html ================================================ Links for poetry

Links for poetry

poetry-0.1.0-py3-none-any.whl
poetry-0.1.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/black.html ================================================ Links for black

Links for black

black-19.10b0-py36-none-any.whl black-19.10b0.tar.gz black-21.11b0-py3-none-any.whl black-21.11b0.tar.gz ================================================ FILE: tests/repositories/fixtures/legacy/clikit.html ================================================ Links for clikit

Links for clikit

clikit-0.2.3-py2.py3-none-any.whl
clikit-0.2.3.tar.gz
clikit-0.2.4-py2.py3-none-any.whl
clikit-0.2.4.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/demo.html ================================================ Simple Index demo-0.1.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/discord-py.html ================================================ Links for discord-py

Links for discord-py

discord.py-2.0.0-py3-none-any.whl
discord.py-2.0.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/futures-partial-yank.html ================================================ Links for futures

Links for futures

futures-3.2.0-py2-none-any.whl
futures-3.2.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/futures.html ================================================ Links for futures

Links for futures

futures-3.2.0-py2-none-any.whl
futures-3.2.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/invalid-version.html ================================================ Links for poetry

Links for poetry

poetry-21.07.28.5ffb65e2ff8067c732e2b178d03b707c7fb27855-py3-none-any.whl
poetry-0.1.0-py3-none-any.whl
================================================ FILE: tests/repositories/fixtures/legacy/ipython.html ================================================ Links for ipython

Links for ipython

ipython-4.1.0rc1-py2.py3-none-any.whl
ipython-4.1.0rc1.tar.gz
ipython-5.7.0-py2-none-any.whl
ipython-5.7.0-py3-none-any.whl
ipython-5.7.0.tar.gz
ipython-7.5.0-py3-none-any.whl
ipython-7.5.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/isort-metadata.html ================================================ Links for isort

Links for isort

isort-metadata-4.3.4-py3-none-any.whl
================================================ FILE: tests/repositories/fixtures/legacy/isort.html ================================================ Links for isort

Links for isort

isort-4.3.4-py2-none-any.whl
isort-4.3.4-py3-none-any.whl
isort-4.3.4.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/json/_readme ================================================ Files from tests/repositories/fixtures/pypi.org/json are used as fallback! ================================================ FILE: tests/repositories/fixtures/legacy/json/absolute.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry-0.1.0-py3-none-any.whl", "hashes": { "sha256": "1d85132efab8ead3c6f69202843da40a03823992091c29f8d65a31af68940163" }, "requires-python": ">=3.6.0", "url": "https://files.pythonhosted.org/packages/e9/df/0ab4afa9c5d9e6b690c5c27c9f50330b98a7ecfe1185ce2dc1b19188b064/poetry-0.1.0-py3-none-any.whl" }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry-0.1.0.tar.gz", "hashes": { "sha256": "db33179244321b0b86c6c3645225ff2062ed3495ca16d0d64b3a5df804a82273" }, "requires-python": ">=3.6.0", "url": "https://files.pythonhosted.org/packages/d5/c5/4efe096ce56505435ccbe8aeefcd5c8c3bb0da211ef8fe58934d087daef2/poetry-0.1.0.tar.gz" } ], "meta": { "_last-serial": 0, "api-version": "1.0" }, "name": "poetry", "project-status": { "status": "active" }, "versions": [ "0.1.0" ] } ================================================ FILE: tests/repositories/fixtures/legacy/json/demo.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "demo-0.1.0.tar.gz", "url": "https://files.pythonhosted.org/distributions/demo-0.1.0.tar.gz" } ], "meta": { "_last-serial": 0, "api-version": "1.0" }, "name": "demo", "project-status": { "status": "active" }, "versions": [ "0.1.0" ] } ================================================ FILE: tests/repositories/fixtures/legacy/json/invalid-version.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry-21.07.28.5ffb65e2ff8067c732e2b178d03b707c7fb27855-py3-none-any.whl", "hashes": { "sha256": "1d85132efab8ead3c6f69202843da40a03823992091c29f8d65a31af68940163" }, "requires-python": ">=3.6.0", "url": "poetry-21.07.28.5ffb65e2ff8067c732e2b178d03b707c7fb27855-py3-none-any.whl" }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry-0.1.0-py3-none-any.whl", "hashes": { "sha256": "1d85132efab8ead3c6f69202843da40a03823992091c29f8d65a31af68940163" }, "requires-python": ">=3.6.0", "url": "poetry-0.1.0-py3-none-any.whl" } ], "meta": { "_last-serial": 0, "api-version": "1.0" }, "name": "poetry", "project-status": { "status": "active" }, "versions": [ "0.1.0", "21.07.28.5ffb65e2ff8067c732e2b178d03b707c7fb27855" ] } ================================================ FILE: tests/repositories/fixtures/legacy/json/isort-metadata.json ================================================ { "name": "isort-metadata", "files": [ { "filename": "isort-metadata-4.3.4-py3-none-any.whl", "url": "https://files.pythonhosted.org/packages/1f/2c/non-existent/isort-metadata-4.3.4-py3-none-any.whl", "dist-info-metadata": { "sha256": "e360bf0ed8a06390513d50dd5b7e9d635c789853a93b84163f9de4ae0647580c" }, "hashes": { "sha256": "1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af" } } ], "meta": { "api-version": "1.0", "_last-serial": 3575149 } } ================================================ FILE: tests/repositories/fixtures/legacy/json/jupyter.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "jupyter-1.0.0.tar.gz", "url": "https://files.pythonhosted.org/packages/c9/a9/371d0b8fe37dd231cf4b2cff0a9f0f25e98f3a73c3771742444be27f2944/jupyter-1.0.0.tar.gz" } ], "meta": { "_last-serial": 0, "api-version": "1.0" }, "name": "jupyter", "project-status": { "status": "active" }, "versions": [ "1.0.0" ] } ================================================ FILE: tests/repositories/fixtures/legacy/json/poetry-test-py2-py3-metadata-merge.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry_test_py2_py3_metadata_merge-0.1.0-py2-none-any.whl", "url": "https://files.pythonhosted.org/packages/52/19/poetry_test_py2_py3_metadata_merge-0.1.0-py2-none-any.whl" }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry_test_py2_py3_metadata_merge-0.1.0-py3-none-any.whl", "url": "https://files.pythonhosted.org/packages/c7/b6/poetry_test_py2_py3_metadata_merge-0.1.0-py3-none-any.whl" } ], "meta": { "_last-serial": 0, "api-version": "1.0" }, "name": "poetry-test-py2-py3-metadata-merge", "project-status": { "status": "active" }, "versions": [ "0.1.0" ] } ================================================ FILE: tests/repositories/fixtures/legacy/json/relative.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry-0.1.0-py3-none-any.whl", "hashes": { "sha256": "1d85132efab8ead3c6f69202843da40a03823992091c29f8d65a31af68940163" }, "requires-python": ">=3.6.0", "url": "poetry-0.1.0-py3-none-any.whl" }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry-0.1.0.tar.gz", "hashes": { "sha256": "db33179244321b0b86c6c3645225ff2062ed3495ca16d0d64b3a5df804a82273" }, "requires-python": ">=3.6.0", "url": "poetry-0.1.0.tar.gz" }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry-0.1.1.tar.bz2", "hashes": { "sha256": "db33179244321b0b86c6c3645225ff2062ed3495ca16d0d64b3a5df804a82273" }, "requires-python": ">=3.6.0", "url": "poetry-0.1.1.tar.bz2" } ], "meta": { "_last-serial": 0, "api-version": "1.0" }, "name": "poetry", "project-status": { "status": "active" }, "versions": [ "0.1.0", "0.1.1" ] } ================================================ FILE: tests/repositories/fixtures/legacy/json/sqlalchemy-legacy.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "sqlalchemy-legacy-4.3.4-py2-none-any.whl", "url": "https://files.pythonhosted.org/packages/41/d8/a945da414f2adc1d9e2f7d6e7445b27f2be42766879062a2e63616ad4199/sqlalchemy-legacy-4.3.4-py2-none-any.whl" } ], "meta": { "_last-serial": 0, "api-version": "1.0" }, "name": "sqlalchemy-legacy", "project-status": { "status": "active" }, "versions": [ "4.3.4" ] } ================================================ FILE: tests/repositories/fixtures/legacy/jupyter.html ================================================ Links for jupyter

Links for jupyter

jupyter-1.0.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/missing-version.html ================================================ Links for poetry

Links for poetry

poetry-0.1.0-py3-none-any.whl
================================================ FILE: tests/repositories/fixtures/legacy/pastel.html ================================================ Links for pastel

Links for pastel

pastel-0.1.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/poetry-test-py2-py3-metadata-merge.html ================================================ Links for poetry-test-py2-py3-metadata-merge

Links for ipython

poetry_test_py2_py3_metadata_merge-0.1.0-py2-none-any.whl
poetry_test_py2_py3_metadata_merge-0.1.0-py3-none-any.whl
================================================ FILE: tests/repositories/fixtures/legacy/pytest-with-extra-packages.html ================================================ Links for pytest

Links for pytest

pytest-3.5.0-py2.py3-none-any.whl
pytest-3.5.0.tar.gz
futures-3.2.0-py2-none-any.whl
pytest-3.10.0-py2.py3-none-any.whl
pytest-3.5.0-py2.py3-none-any.whl
================================================ FILE: tests/repositories/fixtures/legacy/pytest.html ================================================ Links for pytest

Links for pytest

pytest-3.5.0-py2.py3-none-any.whl
pytest-3.5.0.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/python-language-server.html ================================================ Links for python-language-server

Links for python-language-server

python-language-server-0.21.2.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/pyyaml.html ================================================ Links for python-language-server

Links for python-language-server

PyYAML-3.13-cp37-cp37m-win32.whl
PyYAML-3.13.tar.gz
PyYAML-4.2b2.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy/relative.html ================================================ Links for poetry

Links for poetry

poetry-0.1.0-py3-none-any.whl
poetry-0.1.0.tar.gz
poetry-0.1.1.tar.bz
================================================ FILE: tests/repositories/fixtures/legacy/sqlalchemy-legacy.html ================================================ Links for sqlalchemy-legacy

Links for sqlalchemy-legacy

sqlalchemy-legacy-4.3.4-py2-none-any.whl
================================================ FILE: tests/repositories/fixtures/legacy/tomlkit.html ================================================ Links for tomlkit

Links for tomlkit

tomlkit-0.5.2.tar.gz
================================================ FILE: tests/repositories/fixtures/legacy.py ================================================ from __future__ import annotations import json import re from pathlib import Path from typing import TYPE_CHECKING from typing import Any from urllib.parse import urlparse import pytest import responses from packaging.utils import canonicalize_name from poetry.repositories.legacy_repository import LegacyRepository from tests.helpers import FIXTURE_PATH_REPOSITORIES_LEGACY from tests.helpers import FIXTURE_PATH_REPOSITORIES_PYPI if TYPE_CHECKING: from collections.abc import Callable from packaging.utils import NormalizedName from pytest import FixtureRequest from pytest_mock import MockerFixture from requests import PreparedRequest from poetry.repositories.link_sources.base import LinkSource from tests.types import HttpRequestCallback from tests.types import HttpResponse from tests.types import NormalizedNameTransformer from tests.types import SpecializedLegacyRepositoryMocker pytest_plugins = [ "tests.repositories.fixtures.python_hosted", ] class TestLegacyRepository(LegacyRepository): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.json = False @pytest.fixture def legacy_repository_directory() -> Path: return FIXTURE_PATH_REPOSITORIES_LEGACY @pytest.fixture def legacy_package_json_locations() -> list[Path]: return [ FIXTURE_PATH_REPOSITORIES_LEGACY / "json", FIXTURE_PATH_REPOSITORIES_PYPI / "json", ] @pytest.fixture def legacy_repository_package_names(legacy_repository_directory: Path) -> set[str]: return { package_html_file.stem for package_html_file in legacy_repository_directory.glob("*.html") } @pytest.fixture def legacy_repository_index_html( legacy_repository_directory: Path, legacy_repository_package_names: set[str] ) -> str: hrefs = [ f'{name}
' for name in legacy_repository_package_names ] return f""" Legacy Repository {"".join(hrefs)} """ @pytest.fixture def legacy_repository_index_json( legacy_repository_directory: Path, legacy_repository_package_names: set[str] ) -> dict[str, Any]: names = [{"name": name} for name in legacy_repository_package_names] return {"meta": {"api-version": "1.4"}, "projects": names} @pytest.fixture def legacy_repository_url() -> str: return "https://legacy.foo.bar" @pytest.fixture def legacy_repository_html_callback( legacy_repository_directory: Path, legacy_repository_index_html: str, ) -> HttpRequestCallback: def html_callback(request: PreparedRequest) -> HttpResponse: assert request.url if name := Path(urlparse(request.url).path).name: fixture = legacy_repository_directory / f"{name}.html" if not fixture.exists(): return 404, {}, b"Not Found" return 200, {}, fixture.read_bytes() return 200, {}, legacy_repository_index_html.encode("utf-8") return html_callback @pytest.fixture def legacy_repository_json_callback( legacy_package_json_locations: list[Path], legacy_repository_index_json: dict[str, Any], ) -> HttpRequestCallback: def json_callback(request: PreparedRequest) -> HttpResponse: assert request.url headers = {"Content-Type": "application/vnd.pypi.simple.v1+json"} if name := Path(urlparse(request.url).path).name: fixture = Path() for location in legacy_package_json_locations: fixture = location / f"{name}.json" if fixture.exists(): break if not fixture.exists(): return 404, {}, b"Not Found" return 200, headers, fixture.read_bytes() return 200, headers, json.dumps(legacy_repository_index_json).encode("utf-8") return json_callback @pytest.fixture def legacy_repository_html( http: responses.RequestsMock, legacy_repository_url: str, legacy_repository_html_callback: HttpRequestCallback, mock_files_python_hosted: None, ) -> TestLegacyRepository: http.add_callback( responses.GET, re.compile(r"^https://legacy\.(.*)+/?(.*)?$"), callback=legacy_repository_html_callback, ) repo = TestLegacyRepository("legacy", legacy_repository_url, disable_cache=True) repo.json = False return repo @pytest.fixture def legacy_repository_json( http: responses.RequestsMock, legacy_repository_url: str, legacy_repository_json_callback: HttpRequestCallback, mock_files_python_hosted: None, ) -> TestLegacyRepository: http.add_callback( responses.GET, re.compile(r"^https://legacy\.(.*)+/?(.*)?$"), callback=legacy_repository_json_callback, ) repo = TestLegacyRepository("legacy", legacy_repository_url, disable_cache=True) repo.json = True return repo @pytest.fixture(params=["legacy_repository_html", "legacy_repository_json"]) def legacy_repository(request: FixtureRequest) -> TestLegacyRepository: return request.getfixturevalue(request.param) # type: ignore[no-any-return] @pytest.fixture def specialized_legacy_repository_mocker( legacy_repository_html: LegacyRepository, legacy_repository_url: str, mocker: MockerFixture, ) -> SpecializedLegacyRepositoryMocker: """ This is a mocker factory that allows tests cases to intercept and redirect to special case legacy html files by creating an instance of the mocked legacy repository and then mocking its get_page method for special cases. """ def mock( transformer_or_suffix: NormalizedNameTransformer | str, repository_name: str = "special", repository_url: str = legacy_repository_url, ) -> LegacyRepository: specialized_repository = LegacyRepository( repository_name, repository_url, disable_cache=True ) original_get_page = specialized_repository._get_page def _mocked_get_page(name: NormalizedName) -> LinkSource: return original_get_page( canonicalize_name(f"{name}{transformer_or_suffix}") if isinstance(transformer_or_suffix, str) else transformer_or_suffix(name) ) mocker.patch.object(specialized_repository, "get_page", _mocked_get_page) return specialized_repository return mock @pytest.fixture def legacy_repository_with_extra_packages( specialized_legacy_repository_mocker: SpecializedLegacyRepositoryMocker, ) -> LegacyRepository: return specialized_legacy_repository_mocker("-with-extra-packages") @pytest.fixture def legacy_repository_partial_yank( specialized_legacy_repository_mocker: SpecializedLegacyRepositoryMocker, ) -> LegacyRepository: return specialized_legacy_repository_mocker("-partial-yank") @pytest.fixture def get_legacy_dist_url(legacy_repository_directory: Path) -> Callable[[str], str]: def get_url(name: str) -> str: package_name = name.split("-", 1)[0] path = legacy_repository_directory / f"{package_name}.html" if not path.exists(): raise RuntimeError( f"Fixture for {package_name}.html not found in legacy fixtures" ) content = path.read_text(encoding="utf-8") match = re.search(rf' Callable[[str], tuple[int | None, str | None]]: def get_size_and_upload_time(name: str) -> tuple[int | None, str | None]: package_name = name.split("-", 1)[0] fixture_name = f"{package_name}.json" path = Path() for location in legacy_package_json_locations: path = location / fixture_name if path.exists(): break if not path.exists(): raise RuntimeError( f"Fixture for {fixture_name} not found in legacy fixtures" ) with path.open("rb") as f: content = json.load(f) for file in content["files"]: if file["filename"] == name: size = file.get("size") upload_time = file.get("upload-time") return int(size) if size else None, upload_time raise RuntimeError(f"No URL for {name} found in legacy fixture {fixture_name}") return get_size_and_upload_time ================================================ FILE: tests/repositories/fixtures/pypi.org/generate.py ================================================ """ This is a helper script built to generate mocked PyPI json files and release files. Executing the script does the following for a specified list of releases. 1. Fetch relevant project json file from https://pypi.org/simple/. 2. Fetch relevant release json file from https://pypi.org/pypi///json. 3. Download all files (if not otherwise specified) for each release, including .metadata files. 4. Stub (zero-out) all files not relevant for test cases, only sdist and bdist metadata is retained. a, We also retain `__init__.py` files as some packages use it for dynamic version detection when building sdist. b. Some release bdist, notably that of setuptools, wheel and poetry-core are retained as is in the `dist/` directory as these are required for some test cases. c. All stubbed files are written out to `stubbed/` directory. d. All stubbed files produce a consistent hash. 5. New checksums (sha256 and md5) are calculated and replaced in the following locations. a. All mocked json files. b. Installation lock file fixtures. c. Legacy Repository mocked html files. 6. All unwanted files and metadata is removed from any json file written. This includes any release versions removed. 7. A distribution hash getter fixture is generated. The following also applies. 1. Local json files are preferred over remote ones unless `refresh=True` is specified. a. On removal or addition of a new version for a package, the base package must be refreshed. Otherwise, the new files added will not reflect in the file. b. You can also remove the existing file and re-run the script. 2. Download of distributions already present in `dist/` is skipped. 3. The `stubbed/` directory is cleared for each run. """ from __future__ import annotations import dataclasses import hashlib import io import json import logging import os import re import shutil import sys import tarfile import zipfile from functools import cached_property from gzip import GzipFile from pathlib import Path from typing import TYPE_CHECKING from typing import Any from packaging.metadata import parse_email from poetry.core.masonry.utils.helpers import normalize_file_permissions from poetry.core.packages.package import Package from poetry.repositories.pypi_repository import PyPiRepository from tests.helpers import FIXTURE_PATH from tests.helpers import FIXTURE_PATH_DISTRIBUTIONS from tests.helpers import FIXTURE_PATH_INSTALLATION from tests.helpers import FIXTURE_PATH_REPOSITORIES from tests.helpers import FIXTURE_PATH_REPOSITORIES_LEGACY from tests.helpers import FIXTURE_PATH_REPOSITORIES_PYPI if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterator import requests from poetry.core.packages.utils.link import Link ENABLE_RELEASE_JSON = True logger = logging.getLogger("pypi.generator") logger.setLevel(logging.INFO) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(logging.Formatter("%(asctime)s %(message)s")) logger.addHandler(handler) @dataclasses.dataclass(frozen=True) class _ReleaseFileLocations: dist: Path = dataclasses.field( default=FIXTURE_PATH_REPOSITORIES_PYPI.joinpath("dist") ) mocked: Path = dataclasses.field( default=FIXTURE_PATH_REPOSITORIES_PYPI.joinpath("dist", "mocked") ) stubbed: Path = dataclasses.field( default=FIXTURE_PATH_REPOSITORIES_PYPI.joinpath("stubbed") ) demo: Path = dataclasses.field(default=FIXTURE_PATH_DISTRIBUTIONS) RELEASE_FILE_LOCATIONS = _ReleaseFileLocations() @dataclasses.dataclass class ReleaseFileMetadata: path: Path md5: str = dataclasses.field(init=False) sha256: str = dataclasses.field(init=False) def __post_init__(self) -> None: data = self.path.read_bytes() self.sha256 = hashlib.sha256(data).hexdigest() self.md5 = hashlib.md5(data).hexdigest() class _ReleaseFileCollection: def __init__(self, locations: list[Path] | None = None) -> None: self.locations = locations or [ RELEASE_FILE_LOCATIONS.dist, RELEASE_FILE_LOCATIONS.stubbed, ] def filename_exists(self, filename: str) -> bool: return any(location.joinpath(filename).exists() for location in self.locations) def find(self, filename: str) -> ReleaseFileMetadata | None: for location in self.locations: if location.joinpath(filename).exists(): return ReleaseFileMetadata(location) return None def list(self, location: Path | None = None) -> Iterator[ReleaseFileMetadata]: locations = [location] if location is not None else self.locations for candidate in locations: for file in candidate.glob("*.tar.*"): yield ReleaseFileMetadata(file) for file in candidate.glob("*.zip"): yield ReleaseFileMetadata(file) for file in candidate.glob("*.whl"): yield ReleaseFileMetadata(file) RELEASE_FILE_COLLECTION = _ReleaseFileCollection() def generate_distribution_hashes_fixture(files: list[ReleaseFileMetadata]) -> None: fixture_py = FIXTURE_PATH_REPOSITORIES / "distribution_hashes.py" files.sort(key=lambda f: f.path.name) text = ",\n".join( [ f' "{file.path.name}": DistributionHash(\n' f' "{file.sha256}",\n' f' "{file.md5}",\n' f" )" for file in files ] ) logger.info( "Generating fixture file at %s", fixture_py.relative_to(FIXTURE_PATH.parent.parent), ) fixture_py.write_text( f"""# this file is generated by {Path(__file__).relative_to(FIXTURE_PATH.parent.parent).as_posix()} from __future__ import annotations import dataclasses from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from tests.types import DistributionHashGetter @dataclasses.dataclass class DistributionHash: sha256: str = "" md5: str = "" KNOWN_DISTRIBUTION_HASHES = {{ {text}, }} @pytest.fixture def dist_hash_getter() -> DistributionHashGetter: def get_hash(name: str) -> DistributionHash: return KNOWN_DISTRIBUTION_HASHES.get(name, DistributionHash()) return get_hash """, encoding="utf-8", ) def cleanup_legacy_html_hashes(metadata: ReleaseFileMetadata) -> None: for index in FIXTURE_PATH_REPOSITORIES_LEGACY.glob("*.html"): existing_content = index.read_text(encoding="utf-8") content = re.sub( f"{metadata.path.name}#sha256=[A-Fa-f0-9]{{64}}", f"{metadata.path.name}#sha256={metadata.sha256}", existing_content, ) content = re.sub( f'data-dist-info-metadata="sha256=[A-Fa-f0-9]{{64}}">{metadata.path.name}<', f'data-dist-info-metadata="sha256={metadata.sha256}">{metadata.path.name}<', content, ) content = re.sub( f"{metadata.path.name}#md5=[A-Fa-f0-9]{{32}}", f"{metadata.path.name}#md5={metadata.md5}", content, ) content = re.sub( f'data-dist-info-metadata="md5=[A-Fa-f0-9]{{32}}">{metadata.path.name}<', f'data-dist-info-metadata="md5={metadata.md5}">{metadata.path.name}<', content, ) if existing_content != content: logger.info("Rewriting hashes in %s", index) index.write_text(content, encoding="utf-8") def cleanup_installation_fixtures(metadata: ReleaseFileMetadata) -> None: for file in FIXTURE_PATH_INSTALLATION.glob("*.test"): original_content = file.read_text(encoding="utf-8") content = re.sub( f'file = "{metadata.path.name}", hash = "sha256:[A-Fa-f0-9]{{64}}"', f'file = "{metadata.path.name}", hash = "sha256:{metadata.sha256}"', original_content, ) content = re.sub( f'file = "{metadata.path.name}", hash = "md5:[A-Fa-f0-9]{{32}}"', f'file = "{metadata.path.name}", hash = "md5:{metadata.md5}"', content, ) if content != original_content: logger.info("Rewriting hashes in %s", file) file.write_text(content, encoding="utf-8") class FileManager: def __init__(self, pypi: PyPiRepository) -> None: self.pypi = pypi @staticmethod def should_preserve_file_content_check(link: Link) -> Callable[[str], bool]: def sdist_check(filename: str) -> bool: return filename in { "pyproject.toml", "setup.py", "setup.cfg", "PKG-INFO", "__init__.py", "requires.txt", "requirements.txt", "entry_points.txt", "top_level.txt", } bdist_preserve_regex = re.compile(r"^((?!/).)*\.dist-info/((?!/).)*$") def bdist_check(filename: str) -> bool: return bool(bdist_preserve_regex.match(filename)) if link.is_sdist: return sdist_check return bdist_check def process_metadata_file(self, link: Link) -> None: # we enforce the availability of the metadata file link._metadata = True logger.info("Processing metadata file for %s", link.filename) assert link.metadata_url is not None response: requests.Response = self.pypi.session.get( link.metadata_url, raise_for_status=False ) if response.status_code != 200: logger.info("Skipping metadata for %s", link.filename) return None metadata, _ = parse_email(response.content) content = response.content.decode(encoding="utf-8").replace( metadata["description"], "" ) FIXTURE_PATH_REPOSITORIES_PYPI.joinpath( "metadata", f"{link.filename}.metadata" ).write_text(content, encoding="utf-8", newline="\n") def copy_as_is(self, link: Link) -> ReleaseFileMetadata: dst = FIXTURE_PATH_REPOSITORIES_PYPI / "dists" / link.filename logger.info( "Preserving release file from %s to %s", link.url, dst.relative_to(FIXTURE_PATH_REPOSITORIES_PYPI), ) with self.pypi._cached_or_downloaded_file(link) as src: shutil.copy(src, dst) return ReleaseFileMetadata(dst) def process_zipfile(self, link: Link) -> ReleaseFileMetadata: dst = FIXTURE_PATH_REPOSITORIES_PYPI / "stubbed" / link.filename is_protected = self.should_preserve_file_content_check(link) logger.info( "Stubbing release file from %s to %s", link.url, dst.relative_to(FIXTURE_PATH_REPOSITORIES_PYPI), ) with ( self.pypi._cached_or_downloaded_file(link) as src, zipfile.ZipFile( dst, "w", compression=zipfile.ZIP_DEFLATED ) as stubbed_sdist, zipfile.ZipFile(src) as zf, ): for member in zf.infolist(): if not is_protected(member.filename): logger.debug("Stubbing file %s(%s)", link.filename, member.filename) stubbed_sdist.writestr(member, io.BytesIO().getvalue()) elif Path(member.filename).name == "RECORD": # Since unprotected files are stubbed to be zero size, the RECORD file must # be updated to match. stubbed_content = io.StringIO() for line in zf.read(member.filename).decode("utf-8").splitlines(): filename = line.split(",")[0] if is_protected(filename): stubbed_content.write(f"{line}\n") continue stubbed_line = re.sub( ",sha256=.*", ",sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0", line, ) stubbed_content.write(f"{stubbed_line}\n") stubbed_sdist.writestr(member, stubbed_content.getvalue()) else: logger.debug( "Preserving file %s(%s)", link.filename, member.filename ) stubbed_sdist.writestr(member, zf.read(member.filename)) return ReleaseFileMetadata(dst) def process_tarfile(self, link: Link) -> ReleaseFileMetadata: dst = FIXTURE_PATH_REPOSITORIES_PYPI / "stubbed" / link.filename is_protected = self.should_preserve_file_content_check(link) logger.info( "Stubbing release file from %s to %s", link.url, dst.relative_to(FIXTURE_PATH_REPOSITORIES_PYPI), ) with ( self.pypi._cached_or_downloaded_file(link) as src, GzipFile(dst.as_posix(), mode="wb", mtime=0) as gz, tarfile.TarFile( dst, mode="w", fileobj=gz, format=tarfile.PAX_FORMAT ) as dst_tf, tarfile.open(src, "r") as src_tf, ): for member in src_tf.getmembers(): member.uid = 0 member.gid = 0 member.uname = "" member.gname = "" member.mtime = 0 member.mode = normalize_file_permissions(member.mode) if member.isfile() and not is_protected(Path(member.name).name): logger.debug("Stubbing file %s(%s)", link.filename, member.name) file_obj = io.BytesIO() member.size = file_obj.getbuffer().nbytes dst_tf.addfile(member, file_obj) else: logger.debug("Preserving file %s(%s)", link.filename, member.name) dst_tf.addfile(member, src_tf.extractfile(member)) os.utime(dst, (0, 0)) return ReleaseFileMetadata(dst) class Project: def __init__(self, name: str, releases: list[Release]): self.name = name self.releases: list[Release] = releases @property def filenames(self) -> list[str]: filenames = [] for release in self.releases: filenames.extend(release.filenames) return filenames @property def files(self) -> list[ReleaseFileMetadata]: files = [] for release in self.releases: files.extend(release.files) return files @property def versions(self) -> list[str]: return [release.version for release in self.releases] @cached_property def json_path(self) -> Path: return FIXTURE_PATH_REPOSITORIES_PYPI.joinpath("json", f"{self.name}.json") @staticmethod def _finalise_file_item( data: dict[str, Any], files: list[ReleaseFileMetadata] | None = None ) -> dict[str, Any]: filename = data["filename"] for file in files or []: if file.path.name == filename: data["hashes"] = {"md5": file.md5, "sha256": file.sha256} break metadata_file = ( FIXTURE_PATH_REPOSITORIES_PYPI / "metadata" / f"{filename}.metadata" ) if metadata_file.exists(): metadata = ReleaseFileMetadata(metadata_file) for key in ["core-metadata", "data-dist-info-metadata"]: data[key] = {"sha256": metadata.sha256} return data def _finalise(self, data: dict[str, Any]) -> None: files = self.files data["versions"] = self.versions data["files"] = [ self._finalise_file_item(_file, files) for _file in data["files"] if _file["filename"] in self.filenames ] data["meta"]["_last-serial"] = 0 logger.info( "Finalising up %s", self.json_path.relative_to(FIXTURE_PATH_REPOSITORIES_PYPI), ) self.json_path.write_text( json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) for file in files: cleanup_installation_fixtures(file) cleanup_legacy_html_hashes(file) def populate(self, pypi: PyPiRepository) -> None: logger.info("Fetching remote json via https://pypi.org/simple/%s", self.name) data = ( pypi._get( f"simple/{self.name}/", headers={"Accept": "application/vnd.pypi.simple.v1+json"}, ) or {} ) for release in self.releases: release.populate(pypi) self._finalise(data) class Release: def __init__( self, name: str, version: str, download_files: bool = True, stub: bool = True, preserved_files: list[str] | None = None, ): self.name = name self.version = version self.filenames: list[str] = preserved_files or [] self.download_files: bool = download_files self.stub: bool = stub self.files: list[ReleaseFileMetadata] = [] @cached_property def json_path(self) -> Path: return ( FIXTURE_PATH_REPOSITORIES_PYPI / "json" / self.name / f"{self.version}.json" ) @staticmethod def _finalise_file_item( data: dict[str, Any], files: list[ReleaseFileMetadata] | None = None ) -> dict[str, Any]: filename = data["filename"] for file in files or []: if file.path.name == filename: data["digests"] = {"md5": file.md5, "sha256": file.sha256} data["md5_digest"] = file.md5 break return data def _finalise(self, data: dict[str, Any]) -> None: data.get("info", {"description": ""})["description"] = "" if "vulnerabilities" in data: data["vulnerabilities"] = [] data["urls"] = [ self._finalise_file_item(item, self.files) for item in data["urls"] if item["filename"] in self.filenames ] for item in data["urls"]: self._finalise_file_item(item, self.files) data["last_serial"] = 0 logger.info( "Finalising up %s", self.json_path.relative_to(FIXTURE_PATH_REPOSITORIES_PYPI), ) if not self.json_path.parent.exists(): self.json_path.parent.mkdir(parents=True, exist_ok=True) self.json_path.write_text( json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) def populate(self, pypi: PyPiRepository) -> None: fm = FileManager(pypi) links = pypi.find_links_for_package(Package(self.name, self.version)) for link in links: self.filenames.append(link.filename) fm.process_metadata_file(link) if self.download_files: if not self.stub and link.is_wheel: file = fm.copy_as_is(link) elif link.is_wheel or ( link.is_sdist and link.filename.endswith(".zip") ): file = fm.process_zipfile(link) else: file = fm.process_tarfile(link) self.files.append(file) if ENABLE_RELEASE_JSON: logger.info( "Fetching remote json via https://pypi.org/pypi/%s/%s/json", self.name, self.version, ) data = pypi._get(f"pypi/{self.name}/{self.version}/json") or {} self._finalise(data) def cleanup_old_files(releases: dict[str, list[str]]) -> None: json_fixture_path = FIXTURE_PATH_REPOSITORIES_PYPI / "json" for json_file in json_fixture_path.glob("*.json"): if json_file.stem not in releases and json_file.stem not in {"isort-metadata"}: json_file.unlink() for json_file in json_fixture_path.glob("*/*.json"): if json_file.parent.name == "mocked": continue if ( json_file.parent.name not in releases or json_file.stem not in releases[json_file.parent.name] ): logger.info( "Removing unmanaged release file %s", json_file.relative_to(FIXTURE_PATH_REPOSITORIES_PYPI), ) json_file.unlink() if len(list(json_file.parent.iterdir())) == 0: logger.info( "Removing empty directory %s", json_file.parent.relative_to(FIXTURE_PATH_REPOSITORIES_PYPI), ) json_file.parent.rmdir() PROJECTS = [ Project("attrs", releases=[Release("attrs", "17.4.0")]), Project( "black", releases=[Release("black", "19.10b0"), Release("black", "21.11b0")] ), Project("cleo", releases=[Release("cleo", "1.0.0a5")]), Project("clikit", releases=[Release("clikit", "0.2.4")]), # tests.installation.test_installer.test_installer_with_pypi_repository on windows Project("colorama", releases=[Release("colorama", "0.3.9")]), Project("discord-py", releases=[Release("discord-py", "2.0.0")]), Project("funcsigs", releases=[Release("funcsigs", "1.0.2", download_files=False)]), Project("filecache", releases=[Release("filecache", "0.81", download_files=False)]), Project("futures", releases=[Release("futures", "3.2.0")]), # tests.repositories.test_pypi_repository.test_get_release_info_includes_only_supported_types Project( "hbmqtt", releases=[ Release( "hbmqtt", "0.9.6", preserved_files=[ "hbmqtt-0.9.6.linux-x86_64.tar.gz", "hbmqtt-0.9.6-py3.8.egg", ], ) ], ), Project( "importlib-metadata", releases=[Release("importlib-metadata", "1.7.0", download_files=False)], ), Project( "ipython", releases=[ Release("ipython", "4.1.0rc1", download_files=False), # tests.repositories.test_legacy_repository.test_get_package_from_both_py2_and_py3_specific_wheels # tests.repositories.test_legacy_repository.test_get_package_retrieves_non_sha256_hashes_mismatching_known_hash Release("ipython", "5.7.0"), # tests.repositories.test_legacy_repository.test_get_package_retrieves_non_sha256_hashes # tests.repositories.test_legacy_repository.test_get_package_with_dist_and_universal_py3_wheel Release("ipython", "7.5.0"), ], ), # yanked, no dependencies Project("isodate", releases=[Release("isodate", "0.7.0")]), Project("isort", releases=[Release("isort", "4.3.4")]), Project("jupyter", releases=[Release("jupyter", "1.0.0")]), Project("more-itertools", releases=[Release("more-itertools", "4.1.0")]), Project("pastel", releases=[Release("pastel", "0.1.0")]), Project("pluggy", releases=[Release("pluggy", "0.6.0")]), Project( "poetry-core", releases=[ Release("poetry-core", "1.5.0", stub=False), Release("poetry-core", "2.0.1", stub=False), ], ), Project("py", releases=[Release("py", "1.5.3")]), Project("pylev", releases=[Release("pylev", "1.3.0", download_files=False)]), Project( "pytest", releases=[Release("pytest", "3.5.0"), Release("pytest", "3.5.1")] ), # tests.repositories.test_legacy_repository.test_get_package_information_skips_dependencies_with_invalid_constraints Project( "python-language-server", releases=[Release("python-language-server", "0.21.2")] ), Project("pyyaml", releases=[Release("pyyaml", "3.13.0", download_files=False)]), # tests.repositories.test_pypi_repository.test_find_packages Project( "requests", releases=[ Release("requests", "2.18.0", download_files=False), Release("requests", "2.18.1", download_files=False), Release("requests", "2.18.2", download_files=False), Release("requests", "2.18.3", download_files=False), # tests.repositories.test_pypi_repository.test_package Release("requests", "2.18.4", download_files=True), Release("requests", "2.19.0", download_files=False), ], ), Project( "setuptools", releases=[ Release("setuptools", "39.2.0", download_files=False), Release("setuptools", "67.6.1", stub=False), ], ), Project("six", releases=[Release("six", "1.11.0")]), Project("sqlalchemy", releases=[Release("sqlalchemy", "1.2.12")]), # tests.repositories.test_pypi_repository.test_find_packages_with_prereleases Project( "toga", releases=[ Release("toga", "0.3.0", download_files=False), Release("toga", "0.3.0dev1", download_files=False), Release("toga", "0.3.0dev2", download_files=False), Release("toga", "0.4.0", download_files=False), ], ), Project( "tomlkit", releases=[Release("tomlkit", "0.5.2"), Release("tomlkit", "0.5.3")] ), Project("twisted", releases=[Release("twisted", "18.9.0")]), Project("wheel", releases=[Release("wheel", "0.40.0", stub=False)]), Project("zipp", releases=[Release("zipp", "3.5.0")]), ] def main() -> None: pypi = PyPiRepository(disable_cache=False) files: list[ReleaseFileMetadata] = [] releases: dict[str, list[str]] = {} for project in PROJECTS: project = Project(project.name, releases=project.releases) project.populate(pypi) releases[project.name] = project.versions files.extend(project.files) rfc = _ReleaseFileCollection( [RELEASE_FILE_LOCATIONS.demo, RELEASE_FILE_LOCATIONS.mocked] ) files.extend(rfc.list()) generate_distribution_hashes_fixture(files) cleanup_old_files(releases) if __name__ == "__main__": main() ================================================ FILE: tests/repositories/fixtures/pypi.org/json/attrs/17.4.0.json ================================================ { "info": { "author": "Hynek Schlawack", "author_email": "hs@ox.cx", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://www.attrs.org/", "keywords": "class,attribute,boilerplate", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "attrs", "package_url": "https://pypi.org/project/attrs/", "platform": "", "project_url": "https://pypi.org/project/attrs/", "project_urls": { "Homepage": "http://www.attrs.org/" }, "provides_extra": null, "release_url": "https://pypi.org/project/attrs/17.4.0/", "requires_dist": [ "coverage; extra == 'dev'", "hypothesis; extra == 'dev'", "pympler; extra == 'dev'", "pytest; extra == 'dev'", "six; extra == 'dev'", "zope.interface; extra == 'dev'", "sphinx; extra == 'dev'", "zope.interface; extra == 'dev'", "sphinx; extra == 'docs'", "zope.interface; extra == 'docs'", "coverage; extra == 'tests'", "hypothesis; extra == 'tests'", "pympler; extra == 'tests'", "pytest; extra == 'tests'", "six; extra == 'tests'", "zope.interface; extra == 'tests'" ], "requires_python": "", "summary": "Classes Without Boilerplate", "version": "17.4.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "7fe37931797b16c7fa158017457a9ea9", "sha256": "1fbfc10ebc8c876dcbab17f016b80ae1a4f0c1413461a695871427960795beb4" }, "downloads": -1, "filename": "attrs-17.4.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "7fe37931797b16c7fa158017457a9ea9", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 31658, "upload_time": "2017-12-30T08:20:05", "upload_time_iso_8601": "2017-12-30T08:20:05.582456Z", "url": "https://files.pythonhosted.org/packages/b5/60/4e178c1e790fd60f1229a9b3cb2f8bc2f4cc6ff2c8838054c142c70b5adc/attrs-17.4.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "c03e5b3608d9071fbd098850d8922668", "sha256": "eb7536a1e6928190b3008c5b350bdf9850d619fff212341cd096f87a27a5e564" }, "downloads": -1, "filename": "attrs-17.4.0.tar.gz", "has_sig": false, "md5_digest": "c03e5b3608d9071fbd098850d8922668", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 97071, "upload_time": "2017-12-30T08:20:08", "upload_time_iso_8601": "2017-12-30T08:20:08.575620Z", "url": "https://files.pythonhosted.org/packages/8b/0b/a06cfcb69d0cb004fde8bc6f0fd192d96d565d1b8aa2829f0f20adb796e5/attrs-17.4.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/attrs.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "a1828f9b7a019e96302759189410f380814be1dd57a201a56c078f9e8e11a2e5" }, "data-dist-info-metadata": { "sha256": "a1828f9b7a019e96302759189410f380814be1dd57a201a56c078f9e8e11a2e5" }, "filename": "attrs-17.4.0-py2.py3-none-any.whl", "hashes": { "md5": "7fe37931797b16c7fa158017457a9ea9", "sha256": "1fbfc10ebc8c876dcbab17f016b80ae1a4f0c1413461a695871427960795beb4" }, "provenance": null, "requires-python": null, "size": 31658, "upload-time": "2017-12-30T08:20:05.582456Z", "url": "https://files.pythonhosted.org/packages/b5/60/4e178c1e790fd60f1229a9b3cb2f8bc2f4cc6ff2c8838054c142c70b5adc/attrs-17.4.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "attrs-17.4.0.tar.gz", "hashes": { "md5": "c03e5b3608d9071fbd098850d8922668", "sha256": "eb7536a1e6928190b3008c5b350bdf9850d619fff212341cd096f87a27a5e564" }, "provenance": null, "requires-python": null, "size": 97071, "upload-time": "2017-12-30T08:20:08.575620Z", "url": "https://files.pythonhosted.org/packages/8b/0b/a06cfcb69d0cb004fde8bc6f0fd192d96d565d1b8aa2829f0f20adb796e5/attrs-17.4.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "attrs", "project-status": { "status": "active" }, "versions": [ "17.4.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/black/19.10b0.json ================================================ { "info": { "author": "Łukasz Langa", "author_email": "lukasz@langa.pl", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/psf/black", "keywords": "automation formatter yapf autopep8 pyfmt gofmt rustfmt", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "black", "package_url": "https://pypi.org/project/black/", "platform": "", "project_url": "https://pypi.org/project/black/", "project_urls": { "Homepage": "https://github.com/psf/black" }, "provides_extra": null, "release_url": "https://pypi.org/project/black/19.10b0/", "requires_dist": [ "click (>=6.5)", "attrs (>=18.1.0)", "appdirs", "toml (>=0.9.4)", "typed-ast (>=1.4.0)", "regex", "pathspec (<1,>=0.6)", "aiohttp (>=3.3.2) ; extra == 'd'", "aiohttp-cors ; extra == 'd'" ], "requires_python": ">=3.6", "summary": "The uncompromising code formatter.", "version": "19.10b0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "acc537b0f3f7ebf575616490d7cc14f4", "sha256": "13001c5b7dbc81137164b43137320a1785e95ce84e4db849279786877ac6d7f6" }, "downloads": -1, "filename": "black-19.10b0-py36-none-any.whl", "has_sig": false, "md5_digest": "acc537b0f3f7ebf575616490d7cc14f4", "packagetype": "bdist_wheel", "python_version": "py36", "requires_python": ">=3.6", "size": 97525, "upload_time": "2019-10-28T23:53:54", "upload_time_iso_8601": "2019-10-28T23:53:54.000711Z", "url": "https://files.pythonhosted.org/packages/fd/bb/ad34bbc93d1bea3de086d7c59e528d4a503ac8fe318bd1fa48605584c3d2/black-19.10b0-py36-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "c383543109a66a5a99113e6326db5251", "sha256": "6cada614d5d2132698c6d5fff384657273d922c4fffa6a2f0de9e03e25b8913a" }, "downloads": -1, "filename": "black-19.10b0.tar.gz", "has_sig": false, "md5_digest": "c383543109a66a5a99113e6326db5251", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 1019740, "upload_time": "2019-10-28T23:54:05", "upload_time_iso_8601": "2019-10-28T23:54:05.455213Z", "url": "https://files.pythonhosted.org/packages/b0/dc/ecd83b973fb7b82c34d828aad621a6e5865764d52375b8ac1d7a45e23c8d/black-19.10b0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/black/21.11b0.json ================================================ { "info": { "author": "Łukasz Langa", "author_email": "lukasz@langa.pl", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/psf/black", "keywords": "automation formatter yapf autopep8 pyfmt gofmt rustfmt", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "black", "package_url": "https://pypi.org/project/black/", "platform": "", "project_url": "https://pypi.org/project/black/", "project_urls": { "Changelog": "https://github.com/psf/black/blob/main/CHANGES.md", "Homepage": "https://github.com/psf/black" }, "provides_extra": null, "release_url": "https://pypi.org/project/black/21.11b0/", "requires_dist": [ "click (>=7.1.2)", "platformdirs (>=2)", "tomli (<2.0.0,>=0.2.6)", "regex (>=2020.1.8)", "pathspec (<1,>=0.9.0)", "typing-extensions (>=3.10.0.0)", "mypy-extensions (>=0.4.3)", "dataclasses (>=0.6) ; python_version < \"3.7\"", "typed-ast (>=1.4.2) ; python_version < \"3.8\" and implementation_name == \"cpython\"", "typing-extensions (!=3.10.0.1) ; python_version >= \"3.10\"", "colorama (>=0.4.3) ; extra == 'colorama'", "aiohttp (>=3.7.4) ; extra == 'd'", "ipython (>=7.8.0) ; extra == 'jupyter'", "tokenize-rt (>=3.2.0) ; extra == 'jupyter'", "typed-ast (>=1.4.3) ; extra == 'python2'", "uvloop (>=0.15.2) ; extra == 'uvloop'" ], "requires_python": ">=3.6.2", "summary": "The uncompromising code formatter.", "version": "21.11b0", "yanked": true, "yanked_reason": "Broken regex dependency. Use 21.11b1 instead." }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "92942a9efabf8e321a11360667ad2494", "sha256": "38f6ad54069912caf2fa2d4f25d0c5dedef4b2338a0cb545dbe2fdf54a6a8891" }, "downloads": -1, "filename": "black-21.11b0-py3-none-any.whl", "has_sig": false, "md5_digest": "92942a9efabf8e321a11360667ad2494", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6.2", "size": 155131, "upload_time": "2021-11-17T02:32:14", "upload_time_iso_8601": "2021-11-17T02:32:14.551680Z", "url": "https://files.pythonhosted.org/packages/3d/ad/1cf514e7f9ee4c3d8df7c839d7977f7605ad76557f3fca741ec67f76dba6/black-21.11b0-py3-none-any.whl", "yanked": true, "yanked_reason": "Broken regex dependency. Use 21.11b1 instead." }, { "comment_text": "", "digests": { "md5": "f01267bf2613f825dd6684629c1c829e", "sha256": "f23c482185d842e2f19d506e55c004061167e3c677c063ecd721042c62086ada" }, "downloads": -1, "filename": "black-21.11b0.tar.gz", "has_sig": false, "md5_digest": "f01267bf2613f825dd6684629c1c829e", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6.2", "size": 593164, "upload_time": "2021-11-17T02:32:16", "upload_time_iso_8601": "2021-11-17T02:32:16.396821Z", "url": "https://files.pythonhosted.org/packages/2f/db/03e8cef689ab0ff857576ee2ee288d1ff2110ef7f3a77cac62e61f18acaf/black-21.11b0.tar.gz", "yanked": true, "yanked_reason": "Broken regex dependency. Use 21.11b1 instead." } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/black.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "256c27c1f5cc5c7bad6049ff2c339ac9419f5fc00eddbb895cf4780d7b6752b3" }, "data-dist-info-metadata": { "sha256": "256c27c1f5cc5c7bad6049ff2c339ac9419f5fc00eddbb895cf4780d7b6752b3" }, "filename": "black-19.10b0-py36-none-any.whl", "hashes": { "md5": "acc537b0f3f7ebf575616490d7cc14f4", "sha256": "13001c5b7dbc81137164b43137320a1785e95ce84e4db849279786877ac6d7f6" }, "provenance": null, "requires-python": ">=3.6", "size": 97525, "upload-time": "2019-10-28T23:53:54.000711Z", "url": "https://files.pythonhosted.org/packages/fd/bb/ad34bbc93d1bea3de086d7c59e528d4a503ac8fe318bd1fa48605584c3d2/black-19.10b0-py36-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "black-19.10b0.tar.gz", "hashes": { "md5": "c383543109a66a5a99113e6326db5251", "sha256": "6cada614d5d2132698c6d5fff384657273d922c4fffa6a2f0de9e03e25b8913a" }, "provenance": null, "requires-python": ">=3.6", "size": 1019740, "upload-time": "2019-10-28T23:54:05.455213Z", "url": "https://files.pythonhosted.org/packages/b0/dc/ecd83b973fb7b82c34d828aad621a6e5865764d52375b8ac1d7a45e23c8d/black-19.10b0.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "6b5b5209d6862dde7399150665655faa4526565c775f125ad004746fe96d4d4c" }, "data-dist-info-metadata": { "sha256": "6b5b5209d6862dde7399150665655faa4526565c775f125ad004746fe96d4d4c" }, "filename": "black-21.11b0-py3-none-any.whl", "hashes": { "md5": "92942a9efabf8e321a11360667ad2494", "sha256": "38f6ad54069912caf2fa2d4f25d0c5dedef4b2338a0cb545dbe2fdf54a6a8891" }, "provenance": null, "requires-python": ">=3.6.2", "size": 155131, "upload-time": "2021-11-17T02:32:14.551680Z", "url": "https://files.pythonhosted.org/packages/3d/ad/1cf514e7f9ee4c3d8df7c839d7977f7605ad76557f3fca741ec67f76dba6/black-21.11b0-py3-none-any.whl", "yanked": "Broken regex dependency. Use 21.11b1 instead." }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "black-21.11b0.tar.gz", "hashes": { "md5": "f01267bf2613f825dd6684629c1c829e", "sha256": "f23c482185d842e2f19d506e55c004061167e3c677c063ecd721042c62086ada" }, "provenance": null, "requires-python": ">=3.6.2", "size": 593164, "upload-time": "2021-11-17T02:32:16.396821Z", "url": "https://files.pythonhosted.org/packages/2f/db/03e8cef689ab0ff857576ee2ee288d1ff2110ef7f3a77cac62e61f18acaf/black-21.11b0.tar.gz", "yanked": "Broken regex dependency. Use 21.11b1 instead." } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "black", "project-status": { "status": "active" }, "versions": [ "19.10b0", "21.11b0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/cleo/1.0.0a5.json ================================================ { "info": { "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/python-poetry/cleo", "keywords": "cli,commands", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "cleo", "package_url": "https://pypi.org/project/cleo/", "platform": null, "project_url": "https://pypi.org/project/cleo/", "project_urls": { "Homepage": "https://github.com/python-poetry/cleo" }, "provides_extra": null, "release_url": "https://pypi.org/project/cleo/1.0.0a5/", "requires_dist": [ "pylev (>=1.3.0,<2.0.0)", "crashtest (>=0.3.1,<0.4.0)" ], "requires_python": ">=3.7,<4.0", "summary": "Cleo allows you to create beautiful and testable command-line interfaces.", "version": "1.0.0a5", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "19ed7de77063e8f16bc459276ccbe197", "sha256": "d0cfea878b77be28be027033e6af419b705abe47278067a7c3a298f39cf825c5" }, "downloads": -1, "filename": "cleo-1.0.0a5-py3-none-any.whl", "has_sig": false, "md5_digest": "19ed7de77063e8f16bc459276ccbe197", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7,<4.0", "size": 78701, "upload_time": "2022-06-03T20:16:19", "upload_time_iso_8601": "2022-06-03T20:16:19.386916Z", "url": "https://files.pythonhosted.org/packages/45/0c/3825603bf62f360829b1eea29a43dadce30829067e288170b3bf738aafd0/cleo-1.0.0a5-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "92e181952976e09b9d1c583da6c3e2fc", "sha256": "88f0a4275a17f2ab4d013786b8b9522d4c60bd37d8fc9b3def0fb27f4ac1e694" }, "downloads": -1, "filename": "cleo-1.0.0a5.tar.gz", "has_sig": false, "md5_digest": "92e181952976e09b9d1c583da6c3e2fc", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7,<4.0", "size": 61431, "upload_time": "2022-06-03T20:16:21", "upload_time_iso_8601": "2022-06-03T20:16:21.133890Z", "url": "https://files.pythonhosted.org/packages/2f/16/1c1902b225756745f9860451a44a2e2a3c26ee91c72295e83c63df605ed1/cleo-1.0.0a5.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/cleo.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "5627f48cfca57f878bf73ea222fa8ca2f24ad248061a2f0151f22219386838f9" }, "data-dist-info-metadata": { "sha256": "5627f48cfca57f878bf73ea222fa8ca2f24ad248061a2f0151f22219386838f9" }, "filename": "cleo-1.0.0a5-py3-none-any.whl", "hashes": { "md5": "19ed7de77063e8f16bc459276ccbe197", "sha256": "d0cfea878b77be28be027033e6af419b705abe47278067a7c3a298f39cf825c5" }, "provenance": null, "requires-python": ">=3.7,<4.0", "size": 78701, "upload-time": "2022-06-03T20:16:19.386916Z", "url": "https://files.pythonhosted.org/packages/45/0c/3825603bf62f360829b1eea29a43dadce30829067e288170b3bf738aafd0/cleo-1.0.0a5-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "cleo-1.0.0a5.tar.gz", "hashes": { "md5": "92e181952976e09b9d1c583da6c3e2fc", "sha256": "88f0a4275a17f2ab4d013786b8b9522d4c60bd37d8fc9b3def0fb27f4ac1e694" }, "provenance": null, "requires-python": ">=3.7,<4.0", "size": 61431, "upload-time": "2022-06-03T20:16:21.133890Z", "url": "https://files.pythonhosted.org/packages/2f/16/1c1902b225756745f9860451a44a2e2a3c26ee91c72295e83c63df605ed1/cleo-1.0.0a5.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "cleo", "project-status": { "status": "active" }, "versions": [ "1.0.0a5" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/clikit/0.2.4.json ================================================ { "info": { "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/sdispater/clikit", "keywords": "packaging,dependency,poetry", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "Sébastien Eustace", "maintainer_email": "sebastien@eustace.io", "name": "clikit", "package_url": "https://pypi.org/project/clikit/", "platform": "", "project_url": "https://pypi.org/project/clikit/", "project_urls": { "Homepage": "https://github.com/sdispater/clikit", "Repository": "https://github.com/sdispater/clikit" }, "provides_extra": null, "release_url": "https://pypi.org/project/clikit/0.2.4/", "requires_dist": [ "pastel (>=0.1.0,<0.2.0)", "pylev (>=1.3,<2.0)", "typing (>=3.6,<4.0); python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\"", "enum34 (>=1.1,<2.0); python_version >= \"2.7\" and python_version < \"2.8\"" ], "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "CliKit is a group of utilities to build beautiful and testable command line interfaces.", "version": "0.2.4", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "93a51e8bf259c29692e51a7cbca6d664", "sha256": "27316bf6382b04be8fb2f60c85d538fd2b2b03f0f1eba5c88f7d7eddbefc2778" }, "downloads": -1, "filename": "clikit-0.2.4-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "93a51e8bf259c29692e51a7cbca6d664", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 85786, "upload_time": "2019-05-11T17:09:23", "upload_time_iso_8601": "2019-05-11T17:09:23.516387Z", "url": "https://files.pythonhosted.org/packages/7b/0d/bb4c8a2d0edca8c300373ed736fb4680cf73be5be2ff84544dee5f979c14/clikit-0.2.4-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "f7cdbad3508038a04561f646aae68146", "sha256": "0fdd41e86e8b118a8b1e94ef2835925ada541d481c9b3b2fc635fa68713e6125" }, "downloads": -1, "filename": "clikit-0.2.4.tar.gz", "has_sig": false, "md5_digest": "f7cdbad3508038a04561f646aae68146", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 50980, "upload_time": "2019-05-11T17:09:25", "upload_time_iso_8601": "2019-05-11T17:09:25.865051Z", "url": "https://files.pythonhosted.org/packages/c5/33/14fad4c82f256b0ef60dd25d4b6d8145b463da5274fd9cd842f06af318ed/clikit-0.2.4.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/clikit.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "bf1c55d7159f7b783967c3035898bcf4fee282a654fb70efd0fdafc6e782f8b8" }, "data-dist-info-metadata": { "sha256": "bf1c55d7159f7b783967c3035898bcf4fee282a654fb70efd0fdafc6e782f8b8" }, "filename": "clikit-0.2.4-py2.py3-none-any.whl", "hashes": { "md5": "93a51e8bf259c29692e51a7cbca6d664", "sha256": "27316bf6382b04be8fb2f60c85d538fd2b2b03f0f1eba5c88f7d7eddbefc2778" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 85786, "upload-time": "2019-05-11T17:09:23.516387Z", "url": "https://files.pythonhosted.org/packages/7b/0d/bb4c8a2d0edca8c300373ed736fb4680cf73be5be2ff84544dee5f979c14/clikit-0.2.4-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "clikit-0.2.4.tar.gz", "hashes": { "md5": "f7cdbad3508038a04561f646aae68146", "sha256": "0fdd41e86e8b118a8b1e94ef2835925ada541d481c9b3b2fc635fa68713e6125" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 50980, "upload-time": "2019-05-11T17:09:25.865051Z", "url": "https://files.pythonhosted.org/packages/c5/33/14fad4c82f256b0ef60dd25d4b6d8145b463da5274fd9cd842f06af318ed/clikit-0.2.4.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "clikit", "project-status": { "status": "active" }, "versions": [ "0.2.4" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/colorama/0.3.9.json ================================================ { "info": { "author": "Arnon Yaari", "author_email": "tartley@tartley.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Terminals" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/tartley/colorama", "keywords": "color colour terminal text ansi windows crossplatform xplatform", "license": "BSD", "license_expression": null, "license_files": null, "maintainer": null, "maintainer_email": null, "name": "colorama", "package_url": "https://pypi.org/project/colorama/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/colorama/", "project_urls": { "Download": "UNKNOWN", "Homepage": "https://github.com/tartley/colorama" }, "provides_extra": null, "release_url": "https://pypi.org/project/colorama/0.3.9/", "requires_dist": null, "requires_python": null, "summary": "Cross-platform colored terminal text.", "version": "0.3.9", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "8021c861015b5f590be41190bc3f8eed", "sha256": "78a441d2e984c790526cdef1cfd8415a366979ef5b3186771a055b35886953bf" }, "downloads": -1, "filename": "colorama-0.3.9-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "8021c861015b5f590be41190bc3f8eed", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 20181, "upload_time": "2017-04-27T07:12:36", "upload_time_iso_8601": "2017-04-27T07:12:36.597052Z", "url": "https://files.pythonhosted.org/packages/db/c8/7dcf9dbcb22429512708fe3a547f8b6101c0d02137acbd892505aee57adf/colorama-0.3.9-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "8323a5b84fdf7ad810804e51fc256b39", "sha256": "4c5a15209723ce1330a5c193465fe221098f761e9640d823a2ce7c03f983137f" }, "downloads": -1, "filename": "colorama-0.3.9.tar.gz", "has_sig": false, "md5_digest": "8323a5b84fdf7ad810804e51fc256b39", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25053, "upload_time": "2017-04-27T07:12:12", "upload_time_iso_8601": "2017-04-27T07:12:12.351237Z", "url": "https://files.pythonhosted.org/packages/e6/76/257b53926889e2835355d74fec73d82662100135293e17d382e2b74d1669/colorama-0.3.9.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/colorama.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "b44948f8400b680c05976397672aa7542b3bf013a46af8eff2ca8e3a7bef00d8" }, "data-dist-info-metadata": { "sha256": "b44948f8400b680c05976397672aa7542b3bf013a46af8eff2ca8e3a7bef00d8" }, "filename": "colorama-0.3.9-py2.py3-none-any.whl", "hashes": { "md5": "8021c861015b5f590be41190bc3f8eed", "sha256": "78a441d2e984c790526cdef1cfd8415a366979ef5b3186771a055b35886953bf" }, "provenance": null, "requires-python": null, "size": 20181, "upload-time": "2017-04-27T07:12:36.597052Z", "url": "https://files.pythonhosted.org/packages/db/c8/7dcf9dbcb22429512708fe3a547f8b6101c0d02137acbd892505aee57adf/colorama-0.3.9-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "colorama-0.3.9.tar.gz", "hashes": { "md5": "8323a5b84fdf7ad810804e51fc256b39", "sha256": "4c5a15209723ce1330a5c193465fe221098f761e9640d823a2ce7c03f983137f" }, "provenance": null, "requires-python": null, "size": 25053, "upload-time": "2017-04-27T07:12:12.351237Z", "url": "https://files.pythonhosted.org/packages/e6/76/257b53926889e2835355d74fec73d82662100135293e17d382e2b74d1669/colorama-0.3.9.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "colorama", "project-status": { "status": "active" }, "versions": [ "0.3.9" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/discord-py/2.0.0.json ================================================ { "info": { "author": "Rapptz", "author_email": "", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities", "Typing :: Typed" ], "description": "", "description_content_type": "text/x-rst", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/Rapptz/discord.py", "keywords": "", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "discord.py", "package_url": "https://pypi.org/project/discord.py/", "platform": null, "project_url": "https://pypi.org/project/discord.py/", "project_urls": { "Documentation": "https://discordpy.readthedocs.io/en/latest/", "Homepage": "https://github.com/Rapptz/discord.py", "Issue tracker": "https://github.com/Rapptz/discord.py/issues" }, "provides_extra": null, "release_url": "https://pypi.org/project/discord.py/2.0.0/", "requires_dist": [ "aiohttp (<4,>=3.7.4)", "sphinx (==4.4.0) ; extra == 'docs'", "sphinxcontrib-trio (==1.1.2) ; extra == 'docs'", "sphinxcontrib-websupport ; extra == 'docs'", "typing-extensions (<5,>=4.3) ; extra == 'docs'", "orjson (>=3.5.4) ; extra == 'speed'", "aiodns (>=1.1) ; extra == 'speed'", "Brotli ; extra == 'speed'", "cchardet ; extra == 'speed'", "coverage[toml] ; extra == 'test'", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "pytest-cov ; extra == 'test'", "pytest-mock ; extra == 'test'", "typing-extensions (<5,>=4.3) ; extra == 'test'", "PyNaCl (<1.6,>=1.3.0) ; extra == 'voice'" ], "requires_python": ">=3.8.0", "summary": "A Python wrapper for the Discord API", "version": "2.0.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "65394fc868632423cedb6be7259db970", "sha256": "25b9739ba456622655203a0925b354c0ba96ac6c740562e7c37791c2f6b594fb" }, "downloads": -1, "filename": "discord.py-2.0.0-py3-none-any.whl", "has_sig": false, "md5_digest": "65394fc868632423cedb6be7259db970", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.8.0", "size": 1059049, "upload_time": "2022-08-18T03:47:52", "upload_time_iso_8601": "2022-08-18T03:47:52.438785Z", "url": "https://files.pythonhosted.org/packages/0e/d9/7b057cab41c16144925ba4f96dab576a8ebb7b80a98d40e06bd94298eb3b/discord.py-2.0.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "6c0505a6032342b29f31f9979f37d277", "sha256": "b86fa9dd562684f7a52564e6dfe0216f6c172a009c0d86b8dea8bdd6ffa6b1f4" }, "downloads": -1, "filename": "discord.py-2.0.0.tar.gz", "has_sig": false, "md5_digest": "6c0505a6032342b29f31f9979f37d277", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.8.0", "size": 955054, "upload_time": "2022-08-18T03:47:54", "upload_time_iso_8601": "2022-08-18T03:47:54.173712Z", "url": "https://files.pythonhosted.org/packages/4c/73/fb89115b07588bf7a46e9eca972b89dd62b5856abd52297fe130b41d9d63/discord.py-2.0.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/discord-py.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "82a6364f64a513ebbd7108338b20ed5e84cbee20a93e3e02c2cbe2d824d1b54b" }, "data-dist-info-metadata": { "sha256": "82a6364f64a513ebbd7108338b20ed5e84cbee20a93e3e02c2cbe2d824d1b54b" }, "filename": "discord.py-2.0.0-py3-none-any.whl", "hashes": { "md5": "65394fc868632423cedb6be7259db970", "sha256": "25b9739ba456622655203a0925b354c0ba96ac6c740562e7c37791c2f6b594fb" }, "provenance": null, "requires-python": ">=3.8.0", "size": 1059049, "upload-time": "2022-08-18T03:47:52.438785Z", "url": "https://files.pythonhosted.org/packages/0e/d9/7b057cab41c16144925ba4f96dab576a8ebb7b80a98d40e06bd94298eb3b/discord.py-2.0.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "discord.py-2.0.0.tar.gz", "hashes": { "md5": "6c0505a6032342b29f31f9979f37d277", "sha256": "b86fa9dd562684f7a52564e6dfe0216f6c172a009c0d86b8dea8bdd6ffa6b1f4" }, "provenance": null, "requires-python": ">=3.8.0", "size": 955054, "upload-time": "2022-08-18T03:47:54.173712Z", "url": "https://files.pythonhosted.org/packages/4c/73/fb89115b07588bf7a46e9eca972b89dd62b5856abd52297fe130b41d9d63/discord.py-2.0.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "discord-py", "project-status": { "status": "active" }, "versions": [ "2.0.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/filecache/0.81.json ================================================ { "info": { "author": "ubershmekel", "author_email": "ubershmekel@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/ubershmekel/filecache", "keywords": "", "license": "", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "filecache", "package_url": "https://pypi.org/project/filecache/", "platform": "", "project_url": "https://pypi.org/project/filecache/", "project_urls": { "Homepage": "https://github.com/ubershmekel/filecache" }, "provides_extra": null, "release_url": "https://pypi.org/project/filecache/0.81/", "requires_dist": null, "requires_python": "", "summary": "Persistent caching decorator", "version": "0.81", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "eb79f96a2addff21798ea11aa51ae15052514e9ac0ab4ab9470ddd1a0da6fd3e", "md5": "0979123d410d2e411025d2e369a10179", "sha256": "91ce1a42b532d0e9ad75364c13159bafc3015973d4a5a0dbf37e4b4feb194055" }, "downloads": -1, "filename": "filecache-0.81-py3-none-any.whl", "has_sig": false, "md5_digest": "0979123d410d2e411025d2e369a10179", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 4449, "upload_time": "2020-05-29T20:07:06", "upload_time_iso_8601": "2020-05-29T20:07:06.928906Z", "url": "https://files.pythonhosted.org/packages/eb/79/f96a2addff21798ea11aa51ae15052514e9ac0ab4ab9470ddd1a0da6fd3e/filecache-0.81-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "b3f5647f13b1cae32f8d3b84866f6bac688b7923c5d7643b994e5e89865c9a2a", "md5": "f4c8b0e4aba2e37a4d2045a1470fa018", "sha256": "be071ad64937b51f38b03ecd82b9b68c08d0f570cdddb30aa8f90150fe54b30a" }, "downloads": -1, "filename": "filecache-0.81.tar.gz", "has_sig": false, "md5_digest": "f4c8b0e4aba2e37a4d2045a1470fa018", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 6423, "upload_time": "2020-05-29T20:07:07", "upload_time_iso_8601": "2020-05-29T20:07:07.751617Z", "url": "https://files.pythonhosted.org/packages/b3/f5/647f13b1cae32f8d3b84866f6bac688b7923c5d7643b994e5e89865c9a2a/filecache-0.81.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/filecache.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "3fed106198f49b73a473e1322cf26f28137f215c8998209b684c20ce01971505" }, "data-dist-info-metadata": { "sha256": "3fed106198f49b73a473e1322cf26f28137f215c8998209b684c20ce01971505" }, "filename": "filecache-0.81-py3-none-any.whl", "hashes": { "sha256": "91ce1a42b532d0e9ad75364c13159bafc3015973d4a5a0dbf37e4b4feb194055" }, "provenance": null, "requires-python": null, "size": 4449, "upload-time": "2020-05-29T20:07:06.928906Z", "url": "https://files.pythonhosted.org/packages/eb/79/f96a2addff21798ea11aa51ae15052514e9ac0ab4ab9470ddd1a0da6fd3e/filecache-0.81-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "filecache-0.81.tar.gz", "hashes": { "sha256": "be071ad64937b51f38b03ecd82b9b68c08d0f570cdddb30aa8f90150fe54b30a" }, "provenance": null, "requires-python": null, "size": 6423, "upload-time": "2020-05-29T20:07:07.751617Z", "url": "https://files.pythonhosted.org/packages/b3/f5/647f13b1cae32f8d3b84866f6bac688b7923c5d7643b994e5e89865c9a2a/filecache-0.81.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "filecache", "project-status": { "status": "active" }, "versions": [ "0.81" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/funcsigs/1.0.2.json ================================================ { "info": { "author": "Testing Cabal", "author_email": "testing-in-python@lists.idyll.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://funcsigs.readthedocs.org", "keywords": null, "license": "ASL", "license_expression": null, "license_files": null, "maintainer": null, "maintainer_email": null, "name": "funcsigs", "package_url": "https://pypi.org/project/funcsigs/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/funcsigs/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://funcsigs.readthedocs.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/funcsigs/1.0.2/", "requires_dist": null, "requires_python": null, "summary": "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+", "version": "1.0.2", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "69cbf5be453359271714c01b9bd06126eaf2e368f1fddfff30818754b5ac2328", "md5": "701d58358171f34b6d1197de2923a35a", "sha256": "330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca" }, "downloads": -1, "filename": "funcsigs-1.0.2-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "701d58358171f34b6d1197de2923a35a", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 17697, "upload_time": "2016-04-25T22:22:05", "upload_time_iso_8601": "2016-04-25T22:22:05.222685Z", "url": "https://files.pythonhosted.org/packages/69/cb/f5be453359271714c01b9bd06126eaf2e368f1fddfff30818754b5ac2328/funcsigs-1.0.2-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "944adb842e7a0545de1cdb0439bb80e6e42dfe82aaeaadd4072f2263a4fbed23", "md5": "7e583285b1fb8a76305d6d68f4ccc14e", "sha256": "a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" }, "downloads": -1, "filename": "funcsigs-1.0.2.tar.gz", "has_sig": false, "md5_digest": "7e583285b1fb8a76305d6d68f4ccc14e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27947, "upload_time": "2016-04-25T22:22:33", "upload_time_iso_8601": "2016-04-25T22:22:33.882246Z", "url": "https://files.pythonhosted.org/packages/94/4a/db842e7a0545de1cdb0439bb80e6e42dfe82aaeaadd4072f2263a4fbed23/funcsigs-1.0.2.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/funcsigs.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "4288c421dc872125cc7bcfef2cbb953584a95dfc8fb21b766ac038af980559fe" }, "data-dist-info-metadata": { "sha256": "4288c421dc872125cc7bcfef2cbb953584a95dfc8fb21b766ac038af980559fe" }, "filename": "funcsigs-1.0.2-py2.py3-none-any.whl", "hashes": { "sha256": "330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca" }, "provenance": null, "requires-python": null, "size": 17697, "upload-time": "2016-04-25T22:22:05.222685Z", "url": "https://files.pythonhosted.org/packages/69/cb/f5be453359271714c01b9bd06126eaf2e368f1fddfff30818754b5ac2328/funcsigs-1.0.2-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "funcsigs-1.0.2.tar.gz", "hashes": { "sha256": "a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" }, "provenance": null, "requires-python": null, "size": 27947, "upload-time": "2016-04-25T22:22:33.882246Z", "url": "https://files.pythonhosted.org/packages/94/4a/db842e7a0545de1cdb0439bb80e6e42dfe82aaeaadd4072f2263a4fbed23/funcsigs-1.0.2.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "funcsigs", "project-status": { "status": "active" }, "versions": [ "1.0.2" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/futures/3.2.0.json ================================================ { "info": { "author": "Alex Grönholm", "author_email": "alex.gronholm@nextday.fi", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Python Software Foundation License", "Programming Language :: Python :: 2 :: Only", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7" ], "description": "", "description_content_type": null, "docs_url": "https://pythonhosted.org/futures/", "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/agronholm/pythonfutures", "keywords": "", "license": "PSF", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "futures", "package_url": "https://pypi.org/project/futures/", "platform": "", "project_url": "https://pypi.org/project/futures/", "project_urls": { "Homepage": "https://github.com/agronholm/pythonfutures" }, "provides_extra": null, "release_url": "https://pypi.org/project/futures/3.2.0/", "requires_dist": null, "requires_python": ">=2.6, <3", "summary": "Backport of the concurrent.futures package from Python 3", "version": "3.2.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "f81c5c27f3ba2efc008cc96363a81c5e", "sha256": "41353b36198757a766cfc82dc9b60e88ecb28e543dd92473b2cc74fc7bf205af" }, "downloads": -1, "filename": "futures-3.2.0-py2-none-any.whl", "has_sig": false, "md5_digest": "f81c5c27f3ba2efc008cc96363a81c5e", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": ">=2.6, <3", "size": 15847, "upload_time": "2017-11-30T23:22:35", "upload_time_iso_8601": "2017-11-30T23:22:35.590688Z", "url": "https://files.pythonhosted.org/packages/2d/99/b2c4e9d5a30f6471e410a146232b4118e697fa3ffc06d6a65efde84debd0/futures-3.2.0-py2-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "40eb168dab84e606df3fdb7e67fe27b7", "sha256": "baf0d469c9e541b747986b7404cd63a5496955bd0c43a3cc068c449b09b7d4a4" }, "downloads": -1, "filename": "futures-3.2.0.tar.gz", "has_sig": false, "md5_digest": "40eb168dab84e606df3fdb7e67fe27b7", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.6, <3", "size": 27320, "upload_time": "2017-11-30T23:22:36", "upload_time_iso_8601": "2017-11-30T23:22:36.994073Z", "url": "https://files.pythonhosted.org/packages/1f/9e/7b2ff7e965fc654592269f2906ade1c7d705f1bf25b7d469fa153f7d19eb/futures-3.2.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/futures.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "e73888099c1e04d81a9824591e3efc584f5181ff1583e1ce2a90af6d1cc731f8" }, "data-dist-info-metadata": { "sha256": "e73888099c1e04d81a9824591e3efc584f5181ff1583e1ce2a90af6d1cc731f8" }, "filename": "futures-3.2.0-py2-none-any.whl", "hashes": { "md5": "f81c5c27f3ba2efc008cc96363a81c5e", "sha256": "41353b36198757a766cfc82dc9b60e88ecb28e543dd92473b2cc74fc7bf205af" }, "provenance": null, "requires-python": ">=2.6, <3", "size": 15847, "upload-time": "2017-11-30T23:22:35.590688Z", "url": "https://files.pythonhosted.org/packages/2d/99/b2c4e9d5a30f6471e410a146232b4118e697fa3ffc06d6a65efde84debd0/futures-3.2.0-py2-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "futures-3.2.0.tar.gz", "hashes": { "md5": "40eb168dab84e606df3fdb7e67fe27b7", "sha256": "baf0d469c9e541b747986b7404cd63a5496955bd0c43a3cc068c449b09b7d4a4" }, "provenance": null, "requires-python": ">=2.6, <3", "size": 27320, "upload-time": "2017-11-30T23:22:36.994073Z", "url": "https://files.pythonhosted.org/packages/1f/9e/7b2ff7e965fc654592269f2906ade1c7d705f1bf25b7d469fa153f7d19eb/futures-3.2.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "futures", "project-status": { "status": "active" }, "versions": [ "3.2.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/hbmqtt/0.9.6.json ================================================ { "info": { "author": "Nicolas Jouanin", "author_email": "nico@beerfactory.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Communications", "Topic :: Internet" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/beerfactory/hbmqtt", "keywords": "", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "hbmqtt", "package_url": "https://pypi.org/project/hbmqtt/", "platform": "all", "project_url": "https://pypi.org/project/hbmqtt/", "project_urls": { "Homepage": "https://github.com/beerfactory/hbmqtt" }, "provides_extra": null, "release_url": "https://pypi.org/project/hbmqtt/0.9.6/", "requires_dist": null, "requires_python": "", "summary": "MQTT client/broker using Python 3.4 asyncio library", "version": "0.9.6", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "ee27912d1d8c307a72985edf0b6ad9fc1c32e22bfc42efbd0244902c43bd9307", "md5": "e7ecbb2bf3aa2b3b5b2a47dc5289039a", "sha256": "c83ba91dc5cf9a01f83afb5380701504f69bdf9ea1c071f4dfdc1cba412fcd63" }, "downloads": -1, "filename": "hbmqtt-0.9.6.linux-x86_64.tar.gz", "has_sig": false, "md5_digest": "e7ecbb2bf3aa2b3b5b2a47dc5289039a", "packagetype": "bdist_dumb", "python_version": "any", "requires_python": null, "size": 126180, "upload_time": "2020-01-25T14:12:53", "upload_time_iso_8601": "2020-01-25T14:12:53.948778Z", "url": "https://files.pythonhosted.org/packages/ee/27/912d1d8c307a72985edf0b6ad9fc1c32e22bfc42efbd0244902c43bd9307/hbmqtt-0.9.6.linux-x86_64.tar.gz", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "4f4b69014d0fd585b45bfdb77ef0e70bcf035fc8fc798c2a0610fd7cfae56cfd", "md5": "d7896681b8d7d27b53302350b5b3c9c2", "sha256": "57799a933500caadb472000ba0c1e043d4768608cd8142104f89c53930d8613c" }, "downloads": -1, "filename": "hbmqtt-0.9.6-py3.8.egg", "has_sig": false, "md5_digest": "d7896681b8d7d27b53302350b5b3c9c2", "packagetype": "bdist_egg", "python_version": "3.8", "requires_python": null, "size": 186560, "upload_time": "2020-01-25T14:13:44", "upload_time_iso_8601": "2020-01-25T14:13:44.484402Z", "url": "https://files.pythonhosted.org/packages/4f/4b/69014d0fd585b45bfdb77ef0e70bcf035fc8fc798c2a0610fd7cfae56cfd/hbmqtt-0.9.6-py3.8.egg", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "b284e3118882f169aa618a856cd91c5f", "sha256": "379f1d9044997c69308ac2e01621c817b5394e1fbe0696e62538ae2dd0aa7e07" }, "downloads": -1, "filename": "hbmqtt-0.9.6.tar.gz", "has_sig": false, "md5_digest": "b284e3118882f169aa618a856cd91c5f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 74727, "upload_time": "2020-01-25T14:12:45", "upload_time_iso_8601": "2020-01-25T14:12:45.640961Z", "url": "https://files.pythonhosted.org/packages/b4/7c/7e1d47e740915bd628f4038083469c5919e759a638f45abab01e09e933cb/hbmqtt-0.9.6.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/hbmqtt.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "hbmqtt-0.9.6-py3.8.egg", "hashes": { "sha256": "57799a933500caadb472000ba0c1e043d4768608cd8142104f89c53930d8613c" }, "provenance": null, "requires-python": null, "size": 186560, "upload-time": "2020-01-25T14:13:44.484402Z", "url": "https://files.pythonhosted.org/packages/4f/4b/69014d0fd585b45bfdb77ef0e70bcf035fc8fc798c2a0610fd7cfae56cfd/hbmqtt-0.9.6-py3.8.egg", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "hbmqtt-0.9.6.linux-x86_64.tar.gz", "hashes": { "sha256": "c83ba91dc5cf9a01f83afb5380701504f69bdf9ea1c071f4dfdc1cba412fcd63" }, "provenance": null, "requires-python": null, "size": 126180, "upload-time": "2020-01-25T14:12:53.948778Z", "url": "https://files.pythonhosted.org/packages/ee/27/912d1d8c307a72985edf0b6ad9fc1c32e22bfc42efbd0244902c43bd9307/hbmqtt-0.9.6.linux-x86_64.tar.gz", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "hbmqtt-0.9.6.tar.gz", "hashes": { "md5": "b284e3118882f169aa618a856cd91c5f", "sha256": "379f1d9044997c69308ac2e01621c817b5394e1fbe0696e62538ae2dd0aa7e07" }, "provenance": null, "requires-python": null, "size": 74727, "upload-time": "2020-01-25T14:12:45.640961Z", "url": "https://files.pythonhosted.org/packages/b4/7c/7e1d47e740915bd628f4038083469c5919e759a638f45abab01e09e933cb/hbmqtt-0.9.6.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "hbmqtt", "project-status": { "status": "active" }, "versions": [ "0.9.6" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/importlib-metadata/1.7.0.json ================================================ { "info": { "author": "Barry Warsaw", "author_email": "barry@python.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://importlib-metadata.readthedocs.io/", "keywords": "", "license": "Apache Software License", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "importlib-metadata", "package_url": "https://pypi.org/project/importlib-metadata/", "platform": "", "project_url": "https://pypi.org/project/importlib-metadata/", "project_urls": { "Homepage": "http://importlib-metadata.readthedocs.io/" }, "provides_extra": null, "release_url": "https://pypi.org/project/importlib-metadata/1.7.0/", "requires_dist": [ "zipp (>=0.5)", "pathlib2 ; python_version < \"3\"", "contextlib2 ; python_version < \"3\"", "configparser (>=3.5) ; python_version < \"3\"", "sphinx ; extra == 'docs'", "rst.linker ; extra == 'docs'", "packaging ; extra == 'testing'", "pep517 ; extra == 'testing'", "importlib-resources (>=1.3) ; (python_version < \"3.9\") and extra == 'testing'" ], "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", "summary": "Read metadata from Python packages", "version": "1.7.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "8e58cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929", "md5": "8ae1f31228e29443c08e07501a99d1b8", "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" }, "downloads": -1, "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "8ae1f31228e29443c08e07501a99d1b8", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", "size": 31809, "upload_time": "2020-06-26T21:38:16", "upload_time_iso_8601": "2020-06-26T21:38:16.079439Z", "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "e2ae0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38", "md5": "4505ea85600cca1e693a4f8f5dd27ba8", "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83" }, "downloads": -1, "filename": "importlib_metadata-1.7.0.tar.gz", "has_sig": false, "md5_digest": "4505ea85600cca1e693a4f8f5dd27ba8", "packagetype": "sdist", "python_version": "source", "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", "size": 29233, "upload_time": "2020-06-26T21:38:17", "upload_time_iso_8601": "2020-06-26T21:38:17.338581Z", "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/importlib-metadata.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "d176e226c843ad287df418105af818b0545801d6827ab3e800e457913be05260" }, "data-dist-info-metadata": { "sha256": "d176e226c843ad287df418105af818b0545801d6827ab3e800e457913be05260" }, "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl", "hashes": { "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" }, "provenance": null, "requires-python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", "size": 31809, "upload-time": "2020-06-26T21:38:16.079439Z", "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "importlib_metadata-1.7.0.tar.gz", "hashes": { "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83" }, "provenance": null, "requires-python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", "size": 29233, "upload-time": "2020-06-26T21:38:17.338581Z", "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "importlib-metadata", "project-status": { "status": "active" }, "versions": [ "1.7.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/ipython/4.1.0rc1.json ================================================ { "info": { "author": "The IPython Development Team", "author_email": "ipython-dev@scipy.org", "bugtrack_url": null, "classifiers": [ "Framework :: IPython", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Topic :: System :: Shells" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "https://github.com/ipython/ipython/downloads", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://ipython.org", "keywords": "Interactive,Interpreter,Shell,Parallel,Distributed,Web-based computing,Qt console,Embedding", "license": "BSD", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "ipython", "package_url": "https://pypi.org/project/ipython/", "platform": "Linux,Mac OSX,Windows XP/Vista/7/8", "project_url": "https://pypi.org/project/ipython/", "project_urls": { "Download": "https://github.com/ipython/ipython/downloads", "Homepage": "http://ipython.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/ipython/4.1.0rc1/", "requires_dist": [ "pickleshare", "setuptools (>=18.5decorator)", "simplegeneric (>0.8)", "traitlets", "pexpect; sys_platform != \"win32\"", "appnope; sys_platform == \"darwin\"", "gnureadline; sys_platform == \"darwin\" and platform_python_implementation == \"CPython\"", "Sphinx (>=1.3); extra == 'all'", "ipykernel; extra == 'all'", "ipyparallel; extra == 'all'", "ipywidgets; extra == 'all'", "nbconvert; extra == 'all'", "nbformat; extra == 'all'", "nose (>=0.10.1); extra == 'all'", "notebook; extra == 'all'", "qtconsole; extra == 'all'", "requests; extra == 'all'", "testpath; extra == 'all'", "Sphinx (>=1.3); extra == 'doc'", "ipykernel; extra == 'kernel'", "nbconvert; extra == 'nbconvert'", "nbformat; extra == 'nbformat'", "ipywidgets; extra == 'notebook'", "notebook; extra == 'notebook'", "ipyparallel; extra == 'parallel'", "qtconsole; extra == 'qtconsole'", "pyreadline (>=2); sys_platform == \"win32\" and extra == 'terminal'", "nose (>=0.10.1); extra == 'test'", "requests; extra == 'test'", "testpath; extra == 'test'", "mock; python_version == \"2.7\" and extra == 'test'" ], "requires_python": "", "summary": "IPython: Productive Interactive Computing", "version": "4.1.0rc1", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "ac0204a5d372b4e64f9c97b2846646aec1ce4532885005aa4ba51eb20b80e17f", "md5": "512f0431c850c75a12baa9f8c4a9f12f", "sha256": "4d0a08f3fd8837502bf33e9497a5ab28fe63e2fa4201765f378cb139c7a60d5f" }, "downloads": -1, "filename": "ipython-4.1.0rc1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "512f0431c850c75a12baa9f8c4a9f12f", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 736900, "upload_time": "2016-01-26T19:58:35", "upload_time_iso_8601": "2016-01-26T19:58:35.544443Z", "url": "https://files.pythonhosted.org/packages/ac/02/04a5d372b4e64f9c97b2846646aec1ce4532885005aa4ba51eb20b80e17f/ipython-4.1.0rc1-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "a0def71f0c8b8a26ef28cc968fbf1859729ad3e68146cd2eb4a759e9c88da218", "md5": "2aff56d8e78341f64663bcbc81366376", "sha256": "6244a8e3293088ee31c1854abe1a1e7a409cf3ac2fb7579aa9616bdfadd3d4dc" }, "downloads": -1, "filename": "ipython-4.1.0rc1.tar.gz", "has_sig": false, "md5_digest": "2aff56d8e78341f64663bcbc81366376", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 4933377, "upload_time": "2016-01-26T19:58:53", "upload_time_iso_8601": "2016-01-26T19:58:53.938491Z", "url": "https://files.pythonhosted.org/packages/a0/de/f71f0c8b8a26ef28cc968fbf1859729ad3e68146cd2eb4a759e9c88da218/ipython-4.1.0rc1.tar.gz", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "71f09d670266b840b8b921dc7106ecddd892f6fb893424883498e1ba3ec3a3a1", "md5": "a9ff233f176dd99b076b81dc8904ab7a", "sha256": "efa3a5a676648cb18e2a2d3cd6353f3c83f0f704df8eb0eb6ae7d0dcbf187ea1" }, "downloads": -1, "filename": "ipython-4.1.0rc1.zip", "has_sig": false, "md5_digest": "a9ff233f176dd99b076b81dc8904ab7a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 5100723, "upload_time": "2016-01-26T19:59:14", "upload_time_iso_8601": "2016-01-26T19:59:14.449245Z", "url": "https://files.pythonhosted.org/packages/71/f0/9d670266b840b8b921dc7106ecddd892f6fb893424883498e1ba3ec3a3a1/ipython-4.1.0rc1.zip", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/ipython/5.7.0.json ================================================ { "info": { "author": "The IPython Development Team", "author_email": "ipython-dev@python.org", "bugtrack_url": null, "classifiers": [ "Framework :: IPython", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Topic :: System :: Shells" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://ipython.org", "keywords": "Interactive,Interpreter,Shell,Embedding", "license": "BSD", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "ipython", "package_url": "https://pypi.org/project/ipython/", "platform": "Linux", "project_url": "https://pypi.org/project/ipython/", "project_urls": { "Homepage": "https://ipython.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/ipython/5.7.0/", "requires_dist": [ "setuptools (>=18.5)", "decorator", "pickleshare", "simplegeneric (>0.8)", "traitlets (>=4.2)", "prompt-toolkit (<2.0.0,>=1.0.4)", "pygments", "backports.shutil-get-terminal-size; python_version == \"2.7\"", "pathlib2; python_version == \"2.7\" or python_version == \"3.3\"", "pexpect; sys_platform != \"win32\"", "appnope; sys_platform == \"darwin\"", "colorama; sys_platform == \"win32\"", "win-unicode-console (>=0.5); sys_platform == \"win32\" and python_version < \"3.6\"", "nbformat; extra == 'all'", "ipykernel; extra == 'all'", "pygments; extra == 'all'", "testpath; extra == 'all'", "notebook; extra == 'all'", "nbconvert; extra == 'all'", "ipyparallel; extra == 'all'", "qtconsole; extra == 'all'", "Sphinx (>=1.3); extra == 'all'", "requests; extra == 'all'", "nose (>=0.10.1); extra == 'all'", "ipywidgets; extra == 'all'", "Sphinx (>=1.3); extra == 'doc'", "ipykernel; extra == 'kernel'", "nbconvert; extra == 'nbconvert'", "nbformat; extra == 'nbformat'", "notebook; extra == 'notebook'", "ipywidgets; extra == 'notebook'", "ipyparallel; extra == 'parallel'", "qtconsole; extra == 'qtconsole'", "nose (>=0.10.1); extra == 'test'", "requests; extra == 'test'", "testpath; extra == 'test'", "pygments; extra == 'test'", "nbformat; extra == 'test'", "ipykernel; extra == 'test'", "mock; python_version == \"2.7\" and extra == 'test'", "numpy; python_version >= \"3.4\" and extra == 'test'" ], "requires_python": "", "summary": "IPython: Productive Interactive Computing", "version": "5.7.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "20da5e0b1f79dccb37f033a885d798d7", "sha256": "4608e3e0500fe8142659d149891400fc0b9fa250051814b569457ae4688943dc" }, "downloads": -1, "filename": "ipython-5.7.0-py2-none-any.whl", "has_sig": false, "md5_digest": "20da5e0b1f79dccb37f033a885d798d7", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 760413, "upload_time": "2018-05-10T18:56:05", "upload_time_iso_8601": "2018-05-10T18:56:05.083695Z", "url": "https://files.pythonhosted.org/packages/52/19/aadde98d6bde1667d0bf431fb2d22451f880aaa373e0a241c7e7cb5815a0/ipython-5.7.0-py2-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "2844fa01618fe27ab99ad455d605b47d", "sha256": "4292c026552a77b2edc0543941516eddd6fe1a4b681a76ac40b3f585d2fca76f" }, "downloads": -1, "filename": "ipython-5.7.0-py3-none-any.whl", "has_sig": false, "md5_digest": "2844fa01618fe27ab99ad455d605b47d", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 760415, "upload_time": "2018-05-10T18:56:07", "upload_time_iso_8601": "2018-05-10T18:56:07.665559Z", "url": "https://files.pythonhosted.org/packages/c7/b6/03e0b5b0972e6161d16c4cec8d41a20372bd0634f8cb4cc0c984b8a91db6/ipython-5.7.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "01f2808ebe78ff2f28dc39be3aa635ca", "sha256": "4e7fb265e0264498bd0d62c6261936a658bf3d38beb8a7b10cd2c6327c62ac2a" }, "downloads": -1, "filename": "ipython-5.7.0.tar.gz", "has_sig": false, "md5_digest": "01f2808ebe78ff2f28dc39be3aa635ca", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 4977745, "upload_time": "2018-05-10T18:56:17", "upload_time_iso_8601": "2018-05-10T18:56:17.132984Z", "url": "https://files.pythonhosted.org/packages/3c/fd/559fead731a29eaa55cc235c8029807b2520976a937c30e9ee603f3bb566/ipython-5.7.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/ipython/7.5.0.json ================================================ { "info": { "author": "The IPython Development Team", "author_email": "ipython-dev@python.org", "bugtrack_url": null, "classifiers": [ "Framework :: IPython", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: System :: Shells" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://ipython.org", "keywords": "Interactive,Interpreter,Shell,Embedding", "license": "BSD", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "ipython", "package_url": "https://pypi.org/project/ipython/", "platform": "Linux", "project_url": "https://pypi.org/project/ipython/", "project_urls": { "Documentation": "https://ipython.readthedocs.io/", "Funding": "https://numfocus.org/", "Homepage": "https://ipython.org", "Source": "https://github.com/ipython/ipython", "Tracker": "https://github.com/ipython/ipython/issues" }, "provides_extra": null, "release_url": "https://pypi.org/project/ipython/7.5.0/", "requires_dist": [ "setuptools (>=18.5)", "jedi (>=0.10)", "decorator", "pickleshare", "traitlets (>=4.2)", "prompt-toolkit (<2.1.0,>=2.0.0)", "pygments", "backcall", "typing; python_version == \"3.4\"", "pexpect; sys_platform != \"win32\"", "appnope; sys_platform == \"darwin\"", "colorama; sys_platform == \"win32\"", "win-unicode-console (>=0.5); sys_platform == \"win32\" and python_version < \"3.6\"", "nbconvert; extra == 'all'", "ipywidgets; extra == 'all'", "pygments; extra == 'all'", "ipykernel; extra == 'all'", "notebook; extra == 'all'", "ipyparallel; extra == 'all'", "requests; extra == 'all'", "Sphinx (>=1.3); extra == 'all'", "nbformat; extra == 'all'", "nose (>=0.10.1); extra == 'all'", "numpy; extra == 'all'", "testpath; extra == 'all'", "qtconsole; extra == 'all'", "Sphinx (>=1.3); extra == 'doc'", "ipykernel; extra == 'kernel'", "nbconvert; extra == 'nbconvert'", "nbformat; extra == 'nbformat'", "notebook; extra == 'notebook'", "ipywidgets; extra == 'notebook'", "ipyparallel; extra == 'parallel'", "qtconsole; extra == 'qtconsole'", "nose (>=0.10.1); extra == 'test'", "requests; extra == 'test'", "testpath; extra == 'test'", "pygments; extra == 'test'", "nbformat; extra == 'test'", "ipykernel; extra == 'test'", "numpy; extra == 'test'" ], "requires_python": ">=3.5", "summary": "IPython: Productive Interactive Computing", "version": "7.5.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "f40ea889fb7adf989760c5e7a38bd112", "sha256": "1b4c76bf1e8dd9067a4f5ab4695d4c5ad81c30d7d06f7592f4c069c389e37f37" }, "downloads": -1, "filename": "ipython-7.5.0-py3-none-any.whl", "has_sig": false, "md5_digest": "f40ea889fb7adf989760c5e7a38bd112", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.5", "size": 770001, "upload_time": "2019-04-25T15:38:21", "upload_time_iso_8601": "2019-04-25T15:38:21.726776Z", "url": "https://files.pythonhosted.org/packages/a9/2e/41dce4ed129057e05a555a7f9629aa2d5f81fdcd4d16568bc24b75a1d2c9/ipython-7.5.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "0e8c1d7c14f309f6cd2dfd4e48e75cb1", "sha256": "cd2a17ac273fea8bf8953118a2d83bad94f592f0db3e83fff9129a1842e36dbe" }, "downloads": -1, "filename": "ipython-7.5.0.tar.gz", "has_sig": false, "md5_digest": "0e8c1d7c14f309f6cd2dfd4e48e75cb1", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 5118610, "upload_time": "2019-04-25T15:38:33", "upload_time_iso_8601": "2019-04-25T15:38:33.098872Z", "url": "https://files.pythonhosted.org/packages/75/74/9b0ef91c8e356c907bb12297000951acb804583b54eeaddc342c5bad4d96/ipython-7.5.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/ipython.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "16762b4f9bbc29f0aa70a7dbd69dee034a28cbdcd72c7c70f09b5bc6da762592" }, "data-dist-info-metadata": { "sha256": "16762b4f9bbc29f0aa70a7dbd69dee034a28cbdcd72c7c70f09b5bc6da762592" }, "filename": "ipython-4.1.0rc1-py2.py3-none-any.whl", "hashes": { "sha256": "4d0a08f3fd8837502bf33e9497a5ab28fe63e2fa4201765f378cb139c7a60d5f" }, "provenance": null, "requires-python": null, "size": 736900, "upload-time": "2016-01-26T19:58:35.544443Z", "url": "https://files.pythonhosted.org/packages/ac/02/04a5d372b4e64f9c97b2846646aec1ce4532885005aa4ba51eb20b80e17f/ipython-4.1.0rc1-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "ipython-4.1.0rc1.tar.gz", "hashes": { "sha256": "6244a8e3293088ee31c1854abe1a1e7a409cf3ac2fb7579aa9616bdfadd3d4dc" }, "provenance": null, "requires-python": null, "size": 4933377, "upload-time": "2016-01-26T19:58:53.938491Z", "url": "https://files.pythonhosted.org/packages/a0/de/f71f0c8b8a26ef28cc968fbf1859729ad3e68146cd2eb4a759e9c88da218/ipython-4.1.0rc1.tar.gz", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "ipython-4.1.0rc1.zip", "hashes": { "sha256": "efa3a5a676648cb18e2a2d3cd6353f3c83f0f704df8eb0eb6ae7d0dcbf187ea1" }, "provenance": null, "requires-python": null, "size": 5100723, "upload-time": "2016-01-26T19:59:14.449245Z", "url": "https://files.pythonhosted.org/packages/71/f0/9d670266b840b8b921dc7106ecddd892f6fb893424883498e1ba3ec3a3a1/ipython-4.1.0rc1.zip", "yanked": false }, { "core-metadata": { "sha256": "07bb0f56a1a5d60d319a01d196c2593ea972f2285e85e473131282ef64b83e59" }, "data-dist-info-metadata": { "sha256": "07bb0f56a1a5d60d319a01d196c2593ea972f2285e85e473131282ef64b83e59" }, "filename": "ipython-5.7.0-py2-none-any.whl", "hashes": { "md5": "20da5e0b1f79dccb37f033a885d798d7", "sha256": "4608e3e0500fe8142659d149891400fc0b9fa250051814b569457ae4688943dc" }, "provenance": null, "requires-python": null, "size": 760413, "upload-time": "2018-05-10T18:56:05.083695Z", "url": "https://files.pythonhosted.org/packages/52/19/aadde98d6bde1667d0bf431fb2d22451f880aaa373e0a241c7e7cb5815a0/ipython-5.7.0-py2-none-any.whl", "yanked": false }, { "core-metadata": { "sha256": "891e312c0f4969308ab703c5e4437dab51f4cac0b190e1e51045ec4a5cad2de4" }, "data-dist-info-metadata": { "sha256": "891e312c0f4969308ab703c5e4437dab51f4cac0b190e1e51045ec4a5cad2de4" }, "filename": "ipython-5.7.0-py3-none-any.whl", "hashes": { "md5": "2844fa01618fe27ab99ad455d605b47d", "sha256": "4292c026552a77b2edc0543941516eddd6fe1a4b681a76ac40b3f585d2fca76f" }, "provenance": null, "requires-python": null, "size": 760415, "upload-time": "2018-05-10T18:56:07.665559Z", "url": "https://files.pythonhosted.org/packages/c7/b6/03e0b5b0972e6161d16c4cec8d41a20372bd0634f8cb4cc0c984b8a91db6/ipython-5.7.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "ipython-5.7.0.tar.gz", "hashes": { "md5": "01f2808ebe78ff2f28dc39be3aa635ca", "sha256": "4e7fb265e0264498bd0d62c6261936a658bf3d38beb8a7b10cd2c6327c62ac2a" }, "provenance": null, "requires-python": null, "size": 4977745, "upload-time": "2018-05-10T18:56:17.132984Z", "url": "https://files.pythonhosted.org/packages/3c/fd/559fead731a29eaa55cc235c8029807b2520976a937c30e9ee603f3bb566/ipython-5.7.0.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "865b29decaca03261c0df6ce2a374ef5f3df4c3f9f7b3388c534a703d45a12ef" }, "data-dist-info-metadata": { "sha256": "865b29decaca03261c0df6ce2a374ef5f3df4c3f9f7b3388c534a703d45a12ef" }, "filename": "ipython-7.5.0-py3-none-any.whl", "hashes": { "md5": "f40ea889fb7adf989760c5e7a38bd112", "sha256": "1b4c76bf1e8dd9067a4f5ab4695d4c5ad81c30d7d06f7592f4c069c389e37f37" }, "provenance": null, "requires-python": ">=3.5", "size": 770001, "upload-time": "2019-04-25T15:38:21.726776Z", "url": "https://files.pythonhosted.org/packages/a9/2e/41dce4ed129057e05a555a7f9629aa2d5f81fdcd4d16568bc24b75a1d2c9/ipython-7.5.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "ipython-7.5.0.tar.gz", "hashes": { "md5": "0e8c1d7c14f309f6cd2dfd4e48e75cb1", "sha256": "cd2a17ac273fea8bf8953118a2d83bad94f592f0db3e83fff9129a1842e36dbe" }, "provenance": null, "requires-python": ">=3.5", "size": 5118610, "upload-time": "2019-04-25T15:38:33.098872Z", "url": "https://files.pythonhosted.org/packages/75/74/9b0ef91c8e356c907bb12297000951acb804583b54eeaddc342c5bad4d96/ipython-7.5.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "ipython", "project-status": { "status": "active" }, "versions": [ "4.1.0rc1", "5.7.0", "7.5.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/isodate/0.7.0.json ================================================ { "info": { "author": "Gerhard Weis", "author_email": null, "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "", "description_content_type": "text/x-rst", "docs_url": null, "download_url": null, "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": null, "keywords": null, "license": "Copyright (c) 2021, Hugo van Kemenade and contributors Copyright (c) 2009-2018, Gerhard Weis and contributors Copyright (c) 2009, Gerhard Weis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ", "license_expression": null, "license_files": null, "maintainer": null, "maintainer_email": null, "name": "isodate", "package_url": "https://pypi.org/project/isodate/", "platform": null, "project_url": "https://pypi.org/project/isodate/", "project_urls": { "Homepage": "https://github.com/gweis/isodate/" }, "provides_extra": null, "release_url": "https://pypi.org/project/isodate/0.7.0/", "requires_dist": null, "requires_python": null, "summary": "An ISO 8601 date/time/duration parser and formatter", "version": "0.7.0", "yanked": true, "yanked_reason": "fails for py2.7 but is not marked as py3 only." }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "1af9e3ee3f5669186356afd2dbe7ce81", "sha256": "04505f97eb100b66dff1239859e6e04ab913714c453d6ab9591adbf418285847" }, "downloads": -1, "filename": "isodate-0.7.0-py3-none-any.whl", "has_sig": false, "md5_digest": "1af9e3ee3f5669186356afd2dbe7ce81", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 22286, "upload_time": "2024-10-08T02:38:56", "upload_time_iso_8601": "2024-10-08T02:38:56.092325Z", "url": "https://files.pythonhosted.org/packages/8f/90/7fba16c0bbee2ea71c135bbf5a905d4f7873ec982ffbe7305b30c555eec1/isodate-0.7.0-py3-none-any.whl", "yanked": true, "yanked_reason": "fails for py2.7 but is not marked as py3 only." }, { "comment_text": "", "digests": { "md5": "5668b7b7120797f03330363000afc35a", "sha256": "167c3615c0bd2e498c9bae7a1aba5863a17e52299aafd89f17a3a091187dca74" }, "downloads": -1, "filename": "isodate-0.7.0.tar.gz", "has_sig": false, "md5_digest": "5668b7b7120797f03330363000afc35a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29597, "upload_time": "2024-10-08T02:38:58", "upload_time_iso_8601": "2024-10-08T02:38:58.140328Z", "url": "https://files.pythonhosted.org/packages/9b/40/32ce777053517be3032bb2ab3bb216959071ee0c16c761879e75c34a323e/isodate-0.7.0.tar.gz", "yanked": true, "yanked_reason": "fails for py2.7 but is not marked as py3 only." } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/isodate.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "e7f80a7843aedda2ae25b1d351c9cbb50c16e45acf0596f7a6591e76d90049a4" }, "data-dist-info-metadata": { "sha256": "e7f80a7843aedda2ae25b1d351c9cbb50c16e45acf0596f7a6591e76d90049a4" }, "filename": "isodate-0.7.0-py3-none-any.whl", "hashes": { "md5": "1af9e3ee3f5669186356afd2dbe7ce81", "sha256": "04505f97eb100b66dff1239859e6e04ab913714c453d6ab9591adbf418285847" }, "provenance": null, "requires-python": null, "size": 22286, "upload-time": "2024-10-08T02:38:56.092325Z", "url": "https://files.pythonhosted.org/packages/8f/90/7fba16c0bbee2ea71c135bbf5a905d4f7873ec982ffbe7305b30c555eec1/isodate-0.7.0-py3-none-any.whl", "yanked": "fails for py2.7 but is not marked as py3 only." }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "isodate-0.7.0.tar.gz", "hashes": { "md5": "5668b7b7120797f03330363000afc35a", "sha256": "167c3615c0bd2e498c9bae7a1aba5863a17e52299aafd89f17a3a091187dca74" }, "provenance": null, "requires-python": null, "size": 29597, "upload-time": "2024-10-08T02:38:58.140328Z", "url": "https://files.pythonhosted.org/packages/9b/40/32ce777053517be3032bb2ab3bb216959071ee0c16c761879e75c34a323e/isodate-0.7.0.tar.gz", "yanked": "fails for py2.7 but is not marked as py3 only." } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "isodate", "project-status": { "status": "active" }, "versions": [ "0.7.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/isort/4.3.4.json ================================================ { "info": { "author": "Timothy Crosley", "author_email": "timothy.crosley@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 6 - Mature", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", "Topic :: Utilities" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/timothycrosley/isort", "keywords": "Refactor", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "isort", "package_url": "https://pypi.org/project/isort/", "platform": "", "project_url": "https://pypi.org/project/isort/", "project_urls": { "Homepage": "https://github.com/timothycrosley/isort" }, "provides_extra": null, "release_url": "https://pypi.org/project/isort/4.3.4/", "requires_dist": null, "requires_python": "", "summary": "A Python utility / library to sort Python imports.", "version": "4.3.4", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "42bccda292eca3c91eadf3eb781a224f", "sha256": "383c39c10b5db83e8d150ac5b84d74bda96e3a1b06a30257f022dcbcd21f54b9" }, "downloads": -1, "filename": "isort-4.3.4-py2-none-any.whl", "has_sig": false, "md5_digest": "42bccda292eca3c91eadf3eb781a224f", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 45393, "upload_time": "2018-02-12T15:06:38", "upload_time_iso_8601": "2018-02-12T15:06:38.441257Z", "url": "https://files.pythonhosted.org/packages/41/d8/a945da414f2adc1d9e2f7d6e7445b27f2be42766879062a2e63616ad4199/isort-4.3.4-py2-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "6c3b582d7782633ec23917b00a97a2fe", "sha256": "5668dce9fb48544c57ed626982e190c8ea99e3a612850453e9c3b193b9fa2edc" }, "downloads": -1, "filename": "isort-4.3.4-py3-none-any.whl", "has_sig": false, "md5_digest": "6c3b582d7782633ec23917b00a97a2fe", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 45352, "upload_time": "2018-02-12T15:06:20", "upload_time_iso_8601": "2018-02-12T15:06:20.089641Z", "url": "https://files.pythonhosted.org/packages/1f/2c/22eee714d7199ae0464beda6ad5fedec8fee6a2f7ffd1e8f1840928fe318/isort-4.3.4-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "9244631852cf8bd8559f7ab78bf4ec78", "sha256": "234ad07e1e2780c27fa56364eefa734bee991b0d744337ef7e7ce3d5b1b59f39" }, "downloads": -1, "filename": "isort-4.3.4.tar.gz", "has_sig": false, "md5_digest": "9244631852cf8bd8559f7ab78bf4ec78", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 56070, "upload_time": "2018-02-12T15:06:16", "upload_time_iso_8601": "2018-02-12T15:06:16.498194Z", "url": "https://files.pythonhosted.org/packages/b1/de/a628d16fdba0d38cafb3d7e34d4830f2c9cb3881384ce5c08c44762e1846/isort-4.3.4.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/isort-metadata.json ================================================ { "name": "isort-metadata", "files": [ { "filename": "isort-metadata-4.3.4-py2-none-any.whl", "url": "https://files.pythonhosted.org/packages/41/d8/a945da414f2adc1d9e2f7d6e7445b27f2be42766879062a2e63616ad4199/isort-metadata-4.3.4-py2-none-any.whl", "core-metadata": true, "hashes": { "md5": "f0ad7704b6dc947073398ba290c3517f", "sha256": "ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" } }, { "filename": "isort-metadata-4.3.4-py3-none-any.whl", "url": "https://files.pythonhosted.org/packages/1f/2c/22eee714d7199ae0464beda6ad5fedec8fee6a2f7ffd1e8f1840928fe318/isort-metadata-4.3.4-py3-none-any.whl", "core-metadata": true, "hashes": { "md5": "fbaac4cd669ac21ea9e21ab1ea3180db", "sha256": "1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af" } }, { "filename": "isort-metadata-4.3.4.tar.gz", "url": "https://files.pythonhosted.org/packages/b1/de/a628d16fdba0d38cafb3d7e34d4830f2c9cb3881384ce5c08c44762e1846/isort-metadata-4.3.4.tar.gz", "hashes": { "md5": "fb554e9c8f9aa76e333a03d470a5cf52", "sha256": "b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8" } } ], "meta": { "api-version": "1.0", "_last-serial": 3575149 } } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/isort.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "4a736288a055765f102d09f6b8bac7d5a2ba13a82cc9e46e80c3f65ef2497d3c" }, "data-dist-info-metadata": { "sha256": "4a736288a055765f102d09f6b8bac7d5a2ba13a82cc9e46e80c3f65ef2497d3c" }, "filename": "isort-4.3.4-py2-none-any.whl", "hashes": { "md5": "42bccda292eca3c91eadf3eb781a224f", "sha256": "383c39c10b5db83e8d150ac5b84d74bda96e3a1b06a30257f022dcbcd21f54b9" }, "provenance": null, "requires-python": null, "size": 45393, "upload-time": "2018-02-12T15:06:38.441257Z", "url": "https://files.pythonhosted.org/packages/41/d8/a945da414f2adc1d9e2f7d6e7445b27f2be42766879062a2e63616ad4199/isort-4.3.4-py2-none-any.whl", "yanked": false }, { "core-metadata": { "sha256": "bd3a28432bb89f731dc82c08c1ee1497c874e85eb8290371547db13d3adc5a34" }, "data-dist-info-metadata": { "sha256": "bd3a28432bb89f731dc82c08c1ee1497c874e85eb8290371547db13d3adc5a34" }, "filename": "isort-4.3.4-py3-none-any.whl", "hashes": { "md5": "6c3b582d7782633ec23917b00a97a2fe", "sha256": "5668dce9fb48544c57ed626982e190c8ea99e3a612850453e9c3b193b9fa2edc" }, "provenance": null, "requires-python": null, "size": 45352, "upload-time": "2018-02-12T15:06:20.089641Z", "url": "https://files.pythonhosted.org/packages/1f/2c/22eee714d7199ae0464beda6ad5fedec8fee6a2f7ffd1e8f1840928fe318/isort-4.3.4-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "isort-4.3.4.tar.gz", "hashes": { "md5": "9244631852cf8bd8559f7ab78bf4ec78", "sha256": "234ad07e1e2780c27fa56364eefa734bee991b0d744337ef7e7ce3d5b1b59f39" }, "provenance": null, "requires-python": null, "size": 56070, "upload-time": "2018-02-12T15:06:16.498194Z", "url": "https://files.pythonhosted.org/packages/b1/de/a628d16fdba0d38cafb3d7e34d4830f2c9cb3881384ce5c08c44762e1846/isort-4.3.4.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "isort", "project-status": { "status": "active" }, "versions": [ "4.3.4" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/jupyter/1.0.0.json ================================================ { "info": { "author": "Jupyter Development Team", "author_email": "jupyter@googlegroups.org", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://jupyter.org", "keywords": null, "license": "BSD", "license_expression": null, "license_files": null, "maintainer": null, "maintainer_email": null, "name": "jupyter", "package_url": "https://pypi.org/project/jupyter/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/jupyter/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://jupyter.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/jupyter/1.0.0/", "requires_dist": null, "requires_python": null, "summary": "Jupyter metapackage. Install all the Jupyter components in one go.", "version": "1.0.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "27f411f164e0878104d76d868127f76f", "sha256": "1de1f2be45629dd6f7f9558e2385ddf6901849699ef1044c52d171a9b520a420" }, "downloads": -1, "filename": "jupyter-1.0.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "27f411f164e0878104d76d868127f76f", "packagetype": "bdist_wheel", "python_version": "3.4", "requires_python": null, "size": 2736, "upload_time": "2015-08-12T00:42:58", "upload_time_iso_8601": "2015-08-12T00:42:58.951595Z", "url": "https://files.pythonhosted.org/packages/83/df/0f5dd132200728a86190397e1ea87cd76244e42d39ec5e88efd25b2abd7e/jupyter-1.0.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "78acaec88533ea6b6e761e7d086a1d04", "sha256": "3ef1e86ba0556ea5922b846416a41acfd2625830d996c7d06d80c90bed1dc193" }, "downloads": -1, "filename": "jupyter-1.0.0.tar.gz", "has_sig": false, "md5_digest": "78acaec88533ea6b6e761e7d086a1d04", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12916, "upload_time": "2015-08-12T00:43:08", "upload_time_iso_8601": "2015-08-12T00:43:08.537857Z", "url": "https://files.pythonhosted.org/packages/c9/a9/371d0b8fe37dd231cf4b2cff0a9f0f25e98f3a73c3771742444be27f2944/jupyter-1.0.0.tar.gz", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "7b7a957694a73ac0c19fe46c216c0ea0", "sha256": "4a855b9717c3ea24fd8ca4fd91ab5995894aecc4d20e7f39c28786a2c1869fae" }, "downloads": -1, "filename": "jupyter-1.0.0.zip", "has_sig": false, "md5_digest": "7b7a957694a73ac0c19fe46c216c0ea0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 16690, "upload_time": "2015-08-12T00:43:12", "upload_time_iso_8601": "2015-08-12T00:43:12.460314Z", "url": "https://files.pythonhosted.org/packages/fc/21/a372b73e3a498b41b92ed915ada7de2ad5e16631546329c03e484c3bf4e9/jupyter-1.0.0.zip", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/jupyter.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "89666fe631d5e97add0c96b87457f91671b0ce522e4f55d88886507984a5f743" }, "data-dist-info-metadata": { "sha256": "89666fe631d5e97add0c96b87457f91671b0ce522e4f55d88886507984a5f743" }, "filename": "jupyter-1.0.0-py2.py3-none-any.whl", "hashes": { "md5": "27f411f164e0878104d76d868127f76f", "sha256": "1de1f2be45629dd6f7f9558e2385ddf6901849699ef1044c52d171a9b520a420" }, "provenance": null, "requires-python": null, "size": 2736, "upload-time": "2015-08-12T00:42:58.951595Z", "url": "https://files.pythonhosted.org/packages/83/df/0f5dd132200728a86190397e1ea87cd76244e42d39ec5e88efd25b2abd7e/jupyter-1.0.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "jupyter-1.0.0.tar.gz", "hashes": { "md5": "78acaec88533ea6b6e761e7d086a1d04", "sha256": "3ef1e86ba0556ea5922b846416a41acfd2625830d996c7d06d80c90bed1dc193" }, "provenance": null, "requires-python": null, "size": 12916, "upload-time": "2015-08-12T00:43:08.537857Z", "url": "https://files.pythonhosted.org/packages/c9/a9/371d0b8fe37dd231cf4b2cff0a9f0f25e98f3a73c3771742444be27f2944/jupyter-1.0.0.tar.gz", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "jupyter-1.0.0.zip", "hashes": { "md5": "7b7a957694a73ac0c19fe46c216c0ea0", "sha256": "4a855b9717c3ea24fd8ca4fd91ab5995894aecc4d20e7f39c28786a2c1869fae" }, "provenance": null, "requires-python": null, "size": 16690, "upload-time": "2015-08-12T00:43:12.460314Z", "url": "https://files.pythonhosted.org/packages/fc/21/a372b73e3a498b41b92ed915ada7de2ad5e16631546329c03e484c3bf4e9/jupyter-1.0.0.zip", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "jupyter", "project-status": { "status": "active" }, "versions": [ "1.0.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/mocked/invalid-version-package.json ================================================ { "name": "invalid-version-package", "files": [ { "filename": "invalid_version_package-.9-cp27-cp27m-win32.whl", "url": "https://files.pythonhosted.org/packages/12/9b/efdbaa3c9694b6315a4410e0d494ad50c5ade22ce33f4b482bfaea3930fd/invalid_version_package-.9-cp27-cp27m-win32.whl", "hashes": { "md5": "76e2c2e8adea20377d9a7e6b6713c952", "sha256": "8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b" } }, { "filename": "invalid_version_package-3.13-cp27-cp27m-win32.whl", "url": "https://files.pythonhosted.org/packages/b8/2e/9c2285870c9de070a1fa5ede702ab5fb329901b3cc4028c24f44eda27c5f/invalid_version_package-3.13-cp27-cp27m-win32.whl", "hashes": { "md5": "a83441aa7004e474bed6f6daeb61f27a", "sha256": "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f" } } ], "meta": { "api-version": "1.0", "_last-serial": 0 } } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/mocked/isort-metadata/4.3.4.json ================================================ { "info": { "author": "Timothy Crosley", "author_email": "timothy.crosley@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 6 - Mature", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", "Topic :: Utilities" ], "description": ".. image:: https://raw.github.com/timothycrosley/isort/master/logo.png\n :alt: isort\n\n########\n\n.. image:: https://badge.fury.io/py/isort.svg\n :target: https://badge.fury.io/py/isort\n :alt: PyPI version\n\n.. image:: https://travis-ci.org/timothycrosley/isort.svg?branch=master\n :target: https://travis-ci.org/timothycrosley/isort\n :alt: Build Status\n\n\n.. image:: https://coveralls.io/repos/timothycrosley/isort/badge.svg?branch=release%2F2.6.0&service=github\n :target: https://coveralls.io/github/timothycrosley/isort?branch=release%2F2.6.0\n :alt: Coverage\n\n.. image:: https://img.shields.io/github/license/mashape/apistatus.svg\n :target: https://pypi.python.org/pypi/hug/\n :alt: License\n\n.. image:: https://badges.gitter.im/Join%20Chat.svg\n :alt: Join the chat at https://gitter.im/timothycrosley/isort\n :target: https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge\n\n\nisort your python imports for you so you don't have to.\n\nisort is a Python utility / library to sort imports alphabetically, and automatically separated into sections.\nIt provides a command line utility, Python library and `plugins for various editors `_ to quickly sort all your imports.\nIt currently cleanly supports Python 2.7 - 3.6 without any dependencies.\n\n.. image:: https://raw.github.com/timothycrosley/isort/develop/example.gif\n :alt: Example Usage\n\nBefore isort:\n\n.. code-block:: python\n\n from my_lib import Object\n\n print(\"Hey\")\n\n import os\n\n from my_lib import Object3\n\n from my_lib import Object2\n\n import sys\n\n from third_party import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14\n\n import sys\n\n from __future__ import absolute_import\n\n from third_party import lib3\n\n print(\"yo\")\n\nAfter isort:\n\n.. code-block:: python\n\n from __future__ import absolute_import\n\n import os\n import sys\n\n from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8,\n lib9, lib10, lib11, lib12, lib13, lib14, lib15)\n\n from my_lib import Object, Object2, Object3\n\n print(\"Hey\")\n print(\"yo\")\n\nInstalling isort\n================\n\nInstalling isort is as simple as:\n\n.. code-block:: bash\n\n pip install isort\n\nor if you prefer\n\n.. code-block:: bash\n\n easy_install isort\n\nUsing isort\n===========\n\n**From the command line**:\n\n.. code-block:: bash\n\n isort mypythonfile.py mypythonfile2.py\n\nor recursively:\n\n.. code-block:: bash\n\n isort -rc .\n\n*which is equivalent to:*\n\n.. code-block:: bash\n\n isort **/*.py\n\nor to see the proposed changes without applying them:\n\n.. code-block:: bash\n\n isort mypythonfile.py --diff\n\nFinally, to atomically run isort against a project, only applying changes if they don't introduce syntax errors do:\n\n.. code-block:: bash\n\n isort -rc --atomic .\n\n(Note: this is disabled by default as it keeps isort from being able to run against code written using a different version of Python)\n\n**From within Python**:\n\n.. code-block:: bash\n\n from isort import SortImports\n\n SortImports(\"pythonfile.py\")\n\nor:\n\n.. code-block:: bash\n\n from isort import SortImports\n\n new_contents = SortImports(file_contents=old_contents).output\n\n**From within Kate:**\n\n.. code-block:: bash\n\n ctrl+[\n\nor:\n\n.. code-block:: bash\n\n menu > Python > Sort Imports\n\nInstalling isort's Kate plugin\n==============================\n\nFor KDE 4.13+ / Pate 2.0+:\n\n.. code-block:: bash\n\n wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin.py --output-document ~/.kde/share/apps/kate/pate/isort_plugin.py\n wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin_ui.rc --output-document ~/.kde/share/apps/kate/pate/isort_plugin_ui.rc\n wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/katepart_isort.desktop --output-document ~/.kde/share/kde4/services/katepart_isort.desktop\n\nFor all older versions:\n\n.. code-block:: bash\n\n wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin_old.py --output-document ~/.kde/share/apps/kate/pate/isort_plugin.py\n\nYou will then need to restart kate and enable Python Plugins as well as the isort plugin itself.\n\nInstalling isort's for your preferred text editor\n=================================================\n\nSeveral plugins have been written that enable to use isort from within a variety of text-editors.\nYou can find a full list of them `on the isort wiki `_.\nAdditionally, I will enthusiastically accept pull requests that include plugins for other text editors\nand add documentation for them as I am notified.\n\nHow does isort work?\n====================\n\nisort parses specified files for global level import lines (imports outside of try / except blocks, functions, etc..)\nand puts them all at the top of the file grouped together by the type of import:\n\n- Future\n- Python Standard Library\n- Third Party\n- Current Python Project\n- Explicitly Local (. before import, as in: ``from . import x``)\n- Custom Separate Sections (Defined by forced_separate list in configuration file)\n- Custom Sections (Defined by sections list in configuration file)\n\nInside of each section the imports are sorted alphabetically. isort automatically removes duplicate python imports,\nand wraps long from imports to the specified line length (defaults to 80).\n\nWhen will isort not work?\n=========================\n\nIf you ever have the situation where you need to have a try / except block in the middle of top-level imports or if\nyour import order is directly linked to precedence.\n\nFor example: a common practice in Django settings files is importing * from various settings files to form\na new settings file. In this case if any of the imports change order you are changing the settings definition itself.\n\nHowever, you can configure isort to skip over just these files - or even to force certain imports to the top.\n\nConfiguring isort\n=================\n\nIf you find the default isort settings do not work well for your project, isort provides several ways to adjust\nthe behavior.\n\nTo configure isort for a single user create a ``~/.isort.cfg`` file:\n\n.. code-block:: ini\n\n [settings]\n line_length=120\n force_to_top=file1.py,file2.py\n skip=file3.py,file4.py\n known_future_library=future,pies\n known_standard_library=std,std2\n known_third_party=randomthirdparty\n known_first_party=mylib1,mylib2\n indent=' '\n multi_line_output=3\n length_sort=1\n forced_separate=django.contrib,django.utils\n default_section=FIRSTPARTY\n no_lines_before=LOCALFOLDER\n\nAdditionally, you can specify project level configuration simply by placing a ``.isort.cfg`` file at the root of your\nproject. isort will look up to 25 directories up, from the file it is ran against, to find a project specific configuration.\n\nOr, if you prefer, you can add an isort section to your project's ``setup.cfg`` or ``tox.ini`` file with any desired settings.\n\nYou can then override any of these settings by using command line arguments, or by passing in override values to the\nSortImports class.\n\nFinally, as of version 3.0 isort supports editorconfig files using the standard syntax defined here:\nhttp://editorconfig.org/\n\nMeaning you place any standard isort configuration parameters within a .editorconfig file under the ``*.py`` section\nand they will be honored.\n\nFor a full list of isort settings and their meanings `take a look at the isort wiki `_.\n\nMulti line output modes\n=======================\n\nYou will notice above the \"multi_line_output\" setting. This setting defines how from imports wrap when they extend\npast the line_length limit and has 6 possible settings:\n\n**0 - Grid**\n\n.. code-block:: python\n\n from third_party import (lib1, lib2, lib3,\n lib4, lib5, ...)\n\n**1 - Vertical**\n\n.. code-block:: python\n\n from third_party import (lib1,\n lib2,\n lib3\n lib4,\n lib5,\n ...)\n\n**2 - Hanging Indent**\n\n.. code-block:: python\n\n from third_party import \\\n lib1, lib2, lib3, \\\n lib4, lib5, lib6\n\n**3 - Vertical Hanging Indent**\n\n.. code-block:: python\n\n from third_party import (\n lib1,\n lib2,\n lib3,\n lib4,\n )\n\n**4 - Hanging Grid**\n\n.. code-block:: python\n\n from third_party import (\n lib1, lib2, lib3, lib4,\n lib5, ...)\n\n**5 - Hanging Grid Grouped**\n\n.. code-block:: python\n\n from third_party import (\n lib1, lib2, lib3, lib4,\n lib5, ...\n )\n\n**6 - NOQA**\n\n.. code-block:: python\n\n from third_party import lib1, lib2, lib3, ... # NOQA\n\nAlternatively, you can set ``force_single_line`` to ``True`` (``-sl`` on the command line) and every import will appear on its\nown line:\n\n.. code-block:: python\n\n from third_party import lib1\n from third_party import lib2\n from third_party import lib3\n ...\n\nNote: to change the how constant indents appear - simply change the indent property with the following accepted formats:\n* Number of spaces you would like. For example: 4 would cause standard 4 space indentation.\n* Tab\n* A verbatim string with quotes around it.\n\nFor example:\n\n.. code-block:: python\n\n \" \"\n\nis equivalent to 4.\n\nFor the import styles that use parentheses, you can control whether or not to\ninclude a trailing comma after the last import with the ``include_trailing_comma``\noption (defaults to ``False``).\n\nIntelligently Balanced Multi-line Imports\n=========================================\n\nAs of isort 3.1.0 support for balanced multi-line imports has been added.\nWith this enabled isort will dynamically change the import length to the one that produces the most balanced grid,\nwhile staying below the maximum import length defined.\n\nExample:\n\n.. code-block:: python\n\n from __future__ import (absolute_import, division,\n print_function, unicode_literals)\n\nWill be produced instead of:\n\n.. code-block:: python\n\n from __future__ import (absolute_import, division, print_function,\n unicode_literals)\n\nTo enable this set ``balanced_wrapping`` to ``True`` in your config or pass the ``-e`` option into the command line utility.\n\nCustom Sections and Ordering\n============================\n\nYou can change the section order with ``sections`` option from the default of:\n\n.. code-block:: ini\n\n FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER\n\nto your preference:\n\n.. code-block:: ini\n\n sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER\n\nYou also can define your own sections and their order.\n\nExample:\n\n.. code-block:: ini\n\n known_django=django\n known_pandas=pandas,numpy\n sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,PANDAS,FIRSTPARTY,LOCALFOLDER\n\nwould create two new sections with the specified known modules.\n\nThe ``no_lines_before`` option will prevent the listed sections from being split from the previous section by an empty line.\n\nExample:\n\n.. code-block:: ini\n\n sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER\n no_lines_before=LOCALFOLDER\n\nwould produce a section with both FIRSTPARTY and LOCALFOLDER modules combined.\n\nAuto-comment import sections\n============================\n\nSome projects prefer to have import sections uniquely titled to aid in identifying the sections quickly\nwhen visually scanning. isort can automate this as well. To do this simply set the ``import_heading_{section_name}``\nsetting for each section you wish to have auto commented - to the desired comment.\n\nFor Example:\n\n.. code-block:: ini\n\n import_heading_stdlib=Standard Library\n import_heading_firstparty=My Stuff\n\nWould lead to output looking like the following:\n\n.. code-block:: python\n\n # Standard Library\n import os\n import sys\n\n import django.settings\n\n # My Stuff\n import myproject.test\n\nOrdering by import length\n=========================\n\nisort also makes it easy to sort your imports by length, simply by setting the ``length_sort`` option to ``True``.\nThis will result in the following output style:\n\n.. code-block:: python\n\n from evn.util import (\n Pool,\n Dict,\n Options,\n Constant,\n DecayDict,\n UnexpectedCodePath,\n )\n\nSkip processing of imports (outside of configuration)\n=====================================================\n\nTo make isort ignore a single import simply add a comment at the end of the import line containing the text ``isort:skip``:\n\n.. code-block:: python\n\n import module # isort:skip\n\nor:\n\n.. code-block:: python\n\n from xyz import (abc, # isort:skip\n yo,\n hey)\n\nTo make isort skip an entire file simply add ``isort:skip_file`` to the module's doc string:\n\n.. code-block:: python\n\n \"\"\" my_module.py\n Best module ever\n\n isort:skip_file\n \"\"\"\n\n import b\n import a\n\nAdding an import to multiple files\n==================================\n\nisort makes it easy to add an import statement across multiple files, while being assured it's correctly placed.\n\nFrom the command line:\n\n.. code-block:: bash\n\n isort -a \"from __future__ import print_function\" *.py\n\nfrom within Kate:\n\n.. code-block::\n\n ctrl+]\n\nor:\n\n.. code-block::\n\n menu > Python > Add Import\n\nRemoving an import from multiple files\n======================================\n\nisort also makes it easy to remove an import from multiple files, without having to be concerned with how it was originally\nformatted.\n\nFrom the command line:\n\n.. code-block:: bash\n\n isort -r \"os.system\" *.py\n\nfrom within Kate:\n\n.. code-block::\n\n ctrl+shift+]\n\nor:\n\n.. code-block::\n\n menu > Python > Remove Import\n\nUsing isort to verify code\n==========================\n\nThe ``--check-only`` option\n---------------------------\n\nisort can also be used to used to verify that code is correctly formatted by running it with ``-c``.\nAny files that contain incorrectly sorted and/or formatted imports will be outputted to ``stderr``.\n\n.. code-block:: bash\n\n isort **/*.py -c -vb\n\n SUCCESS: /home/timothy/Projects/Open_Source/isort/isort_kate_plugin.py Everything Looks Good!\n ERROR: /home/timothy/Projects/Open_Source/isort/isort/isort.py Imports are incorrectly sorted.\n\nOne great place this can be used is with a pre-commit git hook, such as this one by @acdha:\n\nhttps://gist.github.com/acdha/8717683\n\nThis can help to ensure a certain level of code quality throughout a project.\n\n\nGit hook\n--------\n\nisort provides a hook function that can be integrated into your Git pre-commit script to check\nPython code before committing.\n\nTo cause the commit to fail if there are isort errors (strict mode), include the following in\n``.git/hooks/pre-commit``:\n\n.. code-block:: python\n\n #!/usr/bin/env python\n import sys\n from isort.hooks import git_hook\n\n sys.exit(git_hook(strict=True))\n\nIf you just want to display warnings, but allow the commit to happen anyway, call ``git_hook`` without\nthe `strict` parameter.\n\nSetuptools integration\n----------------------\n\nUpon installation, isort enables a ``setuptools`` command that checks Python files\ndeclared by your project.\n\nRunning ``python setup.py isort`` on the command line will check the files\nlisted in your ``py_modules`` and ``packages``. If any warning is found,\nthe command will exit with an error code:\n\n.. code-block:: bash\n\n $ python setup.py isort\n\nAlso, to allow users to be able to use the command without having to install\nisort themselves, add isort to the setup_requires of your ``setup()`` like so:\n\n.. code-block:: python\n\n setup(\n name=\"project\",\n packages=[\"project\"],\n\n setup_requires=[\n \"isort\"\n ]\n )\n\n\nWhy isort?\n==========\n\nisort simply stands for import sort. It was originally called \"sortImports\" however I got tired of typing the extra\ncharacters and came to the realization camelCase is not pythonic.\n\nI wrote isort because in an organization I used to work in the manager came in one day and decided all code must\nhave alphabetically sorted imports. The code base was huge - and he meant for us to do it by hand. However, being a\nprogrammer - I'm too lazy to spend 8 hours mindlessly performing a function, but not too lazy to spend 16\nhours automating it. I was given permission to open source sortImports and here we are :)\n\n--------------------------------------------\n\nThanks and I hope you find isort useful!\n\n~Timothy Crosley\n", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/timothycrosley/isort", "keywords": "Refactor", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "isort", "package_url": "https://pypi.org/project/isort/", "platform": "", "project_url": "https://pypi.org/project/isort/", "project_urls": { "Homepage": "https://github.com/timothycrosley/isort" }, "release_url": "https://pypi.org/project/isort/4.3.4/", "requires_dist": null, "requires_python": "", "summary": "A Python utility / library to sort Python imports.", "version": "4.3.4", "yanked": false, "yanked_reason": null }, "last_serial": 11968646, "urls": [ { "comment_text": "", "digests": { "md5": "f0ad7704b6dc947073398ba290c3517f", "sha256": "ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" }, "downloads": -1, "filename": "isort-4.3.4-py2-none-any.whl", "has_sig": false, "md5_digest": "f0ad7704b6dc947073398ba290c3517f", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 45393, "upload_time": "2018-02-12T15:06:38", "upload_time_iso_8601": "2018-02-12T15:06:38.441257Z", "url": "https://files.pythonhosted.org/packages/41/d8/a945da414f2adc1d9e2f7d6e7445b27f2be42766879062a2e63616ad4199/isort-4.3.4-py2-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "fbaac4cd669ac21ea9e21ab1ea3180db", "sha256": "1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af" }, "downloads": -1, "filename": "isort-4.3.4-py3-none-any.whl", "has_sig": false, "md5_digest": "fbaac4cd669ac21ea9e21ab1ea3180db", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 45352, "upload_time": "2018-02-12T15:06:20", "upload_time_iso_8601": "2018-02-12T15:06:20.089641Z", "url": "https://files.pythonhosted.org/packages/1f/2c/22eee714d7199ae0464beda6ad5fedec8fee6a2f7ffd1e8f1840928fe318/isort-4.3.4-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "fb554e9c8f9aa76e333a03d470a5cf52", "sha256": "b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8" }, "downloads": -1, "filename": "isort-4.3.4.tar.gz", "has_sig": false, "md5_digest": "fb554e9c8f9aa76e333a03d470a5cf52", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 56070, "upload_time": "2018-02-12T15:06:16", "upload_time_iso_8601": "2018-02-12T15:06:16.498194Z", "url": "https://files.pythonhosted.org/packages/b1/de/a628d16fdba0d38cafb3d7e34d4830f2c9cb3881384ce5c08c44762e1846/isort-4.3.4.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/mocked/six-unknown-version/1.11.0.json ================================================ { "info": { "author": "Benjamin Peterson", "author_email": "benjamin@python.org", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries", "Topic :: Utilities" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pypi.python.org/pypi/six/", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "six-unknown-version", "package_url": "https://pypi.org/project/six/", "platform": "", "project_url": "https://pypi.org/project/six/", "project_urls": { "Homepage": "http://pypi.python.org/pypi/six/" }, "provides_extra": null, "release_url": "https://pypi.org/project/six/1.11.0/", "requires_dist": null, "requires_python": "", "summary": "Python 2 and 3 compatibility utilities", "version": "1.11.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "b126a063c665f4c66f23bfd8dc6ebff5", "sha256": "1d9f63b87ee9f0aee49e9d9f8d7883319836bc43f17321b488bc38933827d2c0" }, "downloads": -1, "filename": "six_unknown_version-1.11.0.tar.gz", "has_sig": false, "md5_digest": "25d3568604f921dd23532b88a0ce17e7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29860, "upload_time": "2017-09-17T18:46:54", "upload_time_iso_8601": "2017-09-17T18:46:54.492027Z", "url": "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six_unknown_version-1.11.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/mocked/six-unknown-version.json ================================================ { "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "six_unknown_version-1.11.0.tar.gz", "hashes": { "md5": "b126a063c665f4c66f23bfd8dc6ebff5", "sha256": "1d9f63b87ee9f0aee49e9d9f8d7883319836bc43f17321b488bc38933827d2c0" }, "requires-python": null, "size": 29860, "upload-time": "2017-09-17T18:46:54.492027Z", "url": "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six_unknown_version-1.11.0.tar.gz", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "six_unknown_version-unknown.tar.gz", "hashes": { "md5": "25d3568604f921dd23532b88a0ce17e7", "sha256": "268a4ccb159c1a2d2c79336b02e75058387b0cdbb4cea2f07846a758f48a356d" }, "requires-python": null, "size": 29860, "upload-time": "2017-09-17T18:46:54.492027Z", "url": "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six_unknown_version-unknown.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.1" }, "name": "six-unknown-version", "versions": [ "1.11.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/mocked/with-extra-dependency/0.12.4.json ================================================ { "info": { "author": "Benjamin Peterson", "author_email": "benjamin@python.org", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries", "Topic :: Utilities" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pypi.python.org/pypi/six/", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "with-extra-dependency", "package_url": "https://pypi.org/project/six/", "platform": "", "project_url": "https://pypi.org/project/six/", "project_urls": { "Homepage": "http://pypi.python.org/pypi/six/" }, "provides_extra": null, "release_url": "https://pypi.org/project/six/0.12.4/", "requires_dist": [ "filecache; extra == 'filecache'" ], "requires_python": "", "summary": "Python 2 and 3 compatibility utilities", "version": "0.12.4", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "b126a063c665f4c66f23bfd8dc6ebff5", "sha256": "1d9f63b87ee9f0aee49e9d9f8d7883319836bc43f17321b488bc38933827d2c0" }, "downloads": -1, "filename": "with_extra_dependency-0.12.4.tar.gz", "has_sig": false, "md5_digest": "25d3568604f921dd23532b88a0ce17e7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29860, "upload_time": "2017-09-17T18:46:54", "upload_time_iso_8601": "2017-09-17T18:46:54.492027Z", "url": "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/with_extra_dependency-0.12.4.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/mocked/with-extra-dependency.json ================================================ { "files": [ { "core-metadata": { "sha256": "e98cf244e0e09b017a43e1f03ac551c6b04755176e7c06ce52d6ceddc25cfad2" }, "data-dist-info-metadata": { "sha256": "e98cf244e0e09b017a43e1f03ac551c6b04755176e7c06ce52d6ceddc25cfad2" }, "filename": "with_extra_dependency-0.12.4-py3-none-any.whl", "hashes": { "md5": "632fcf45cc28aed4a4dce1324d1bd1d1", "sha256": "d88b34efa115392ee42c55d6f82cdf5e5e08221ef2e18a16ae696a80008c3499" }, "requires-python": null, "size": 6718, "upload-time": "2017-02-01T23:26:20.148511Z", "url": "https://files.pythonhosted.org/packages/9b/7e/7d701686013c0d7dae62e0977467232a6adc2e562c23878eb3cd4f97d02e/with_extra_dependency-0.12.4-py3-none-any.whl", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.1" }, "name": "with-extra-dependency", "versions": [ "0.12.4" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/mocked/with-transitive-extra-dependency/0.12.4.json ================================================ { "info": { "author": "Benjamin Peterson", "author_email": "benjamin@python.org", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries", "Topic :: Utilities" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pypi.python.org/pypi/six/", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "with-transitive-extra-dependency", "package_url": "https://pypi.org/project/six/", "platform": "", "project_url": "https://pypi.org/project/six/", "project_urls": { "Homepage": "http://pypi.python.org/pypi/six/" }, "provides_extra": null, "release_url": "https://pypi.org/project/six/0.12.4/", "requires_dist": [ "with-extra-dependency[filecache] (>=0.12.4,<0.13.0)" ], "requires_python": "", "summary": "Python 2 and 3 compatibility utilities", "version": "0.12.4", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "b126a063c665f4c66f23bfd8dc6ebff5", "sha256": "1d9f63b87ee9f0aee49e9d9f8d7883319836bc43f17321b488bc38933827d2c0" }, "downloads": -1, "filename": "with_transitive_extra_dependency-0.12.4.tar.gz", "has_sig": false, "md5_digest": "25d3568604f921dd23532b88a0ce17e7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29860, "upload_time": "2017-09-17T18:46:54", "upload_time_iso_8601": "2017-09-17T18:46:54.492027Z", "url": "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/with_transitive_extra_dependency-0.12.4.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/mocked/with-transitive-extra-dependency.json ================================================ { "files": [ { "core-metadata": { "sha256": "2e0d683c7b4e771e915184e69ed28e4ce7c239ca89b9025f7e74d63b37c656b6" }, "data-dist-info-metadata": { "sha256": "2e0d683c7b4e771e915184e69ed28e4ce7c239ca89b9025f7e74d63b37c656b6" }, "filename": "with_transitive_extra_dependency-0.12.4-py3-none-any.whl", "hashes": { "md5": "632fcf45cc28aed4a4dce1324d1bd1d1", "sha256": "d88b34efa115392ee42c55d6f82cdf5e5e08221ef2e18a16ae696a80008c3499" }, "requires-python": null, "size": 6718, "upload-time": "2017-02-01T23:26:20.148511Z", "url": "https://files.pythonhosted.org/packages/9b/7e/7d701686013c0d7dae62e0977467232a6adc2e562c23878eb3cd4f97d02e/with_transitive_extra_dependency-0.12.4-py3-none-any.whl", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.1" }, "name": "with-transitive-extra-dependency", "versions": [ "0.12.4" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/more-itertools/4.1.0.json ================================================ { "info": { "author": "Erik Rose", "author_email": "erikrose@grinchcentral.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries" ], "description": "", "description_content_type": null, "docs_url": "https://pythonhosted.org/more-itertools/", "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/erikrose/more-itertools", "keywords": "itertools,iterator,iteration,filter,peek,peekable,collate,chunk,chunked", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "more-itertools", "package_url": "https://pypi.org/project/more-itertools/", "platform": "", "project_url": "https://pypi.org/project/more-itertools/", "project_urls": { "Homepage": "https://github.com/erikrose/more-itertools" }, "provides_extra": null, "release_url": "https://pypi.org/project/more-itertools/4.1.0/", "requires_dist": [ "six (<2.0.0,>=1.0.0)" ], "requires_python": "", "summary": "More routines for operating on iterables, beyond itertools", "version": "4.1.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "703e1e0922de1f11823da60af1488b7a", "sha256": "0f461c2cd4ec16611396f9ee57f40433de3d59e95475d84c0c829cde02f746cd" }, "downloads": -1, "filename": "more_itertools-4.1.0-py2-none-any.whl", "has_sig": false, "md5_digest": "703e1e0922de1f11823da60af1488b7a", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 47987, "upload_time": "2018-01-21T15:34:19", "upload_time_iso_8601": "2018-01-21T15:34:19.304356Z", "url": "https://files.pythonhosted.org/packages/4a/88/c28e2a2da8f3dc3a391d9c97ad949f2ea0c05198222e7e6af176e5bf9b26/more_itertools-4.1.0-py2-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "ae17a45d13e9dc319794c40fa739c38f", "sha256": "580b6002d1f28feb5bcb8303278d59cf17dfbd19a63a5c2375112dae72c9bf98" }, "downloads": -1, "filename": "more_itertools-4.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "ae17a45d13e9dc319794c40fa739c38f", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 47988, "upload_time": "2018-01-21T15:34:20", "upload_time_iso_8601": "2018-01-21T15:34:20.567853Z", "url": "https://files.pythonhosted.org/packages/7a/46/886917c6a4ce49dd3fff250c01c5abac5390d57992751384fe61befc4877/more_itertools-4.1.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "bf351a1050242ce3af7e475a4da1a26b", "sha256": "bab2dc6f4be8f9a4a72177842c5283e2dff57c167439a03e3d8d901e854f0f2e" }, "downloads": -1, "filename": "more-itertools-4.1.0.tar.gz", "has_sig": false, "md5_digest": "bf351a1050242ce3af7e475a4da1a26b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 51310, "upload_time": "2018-01-21T15:34:22", "upload_time_iso_8601": "2018-01-21T15:34:22.533243Z", "url": "https://files.pythonhosted.org/packages/db/0b/f5660bf6299ec5b9f17bd36096fa8148a1c843fa77ddfddf9bebac9301f7/more-itertools-4.1.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/more-itertools.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "more-itertools-4.1.0.tar.gz", "hashes": { "md5": "bf351a1050242ce3af7e475a4da1a26b", "sha256": "bab2dc6f4be8f9a4a72177842c5283e2dff57c167439a03e3d8d901e854f0f2e" }, "provenance": null, "requires-python": null, "size": 51310, "upload-time": "2018-01-21T15:34:22.533243Z", "url": "https://files.pythonhosted.org/packages/db/0b/f5660bf6299ec5b9f17bd36096fa8148a1c843fa77ddfddf9bebac9301f7/more-itertools-4.1.0.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "59c3513ac4a78ad61b2a0eda88807985c4bb71cd36e6917f2f9c0ea121a7c029" }, "data-dist-info-metadata": { "sha256": "59c3513ac4a78ad61b2a0eda88807985c4bb71cd36e6917f2f9c0ea121a7c029" }, "filename": "more_itertools-4.1.0-py2-none-any.whl", "hashes": { "md5": "703e1e0922de1f11823da60af1488b7a", "sha256": "0f461c2cd4ec16611396f9ee57f40433de3d59e95475d84c0c829cde02f746cd" }, "provenance": null, "requires-python": null, "size": 47987, "upload-time": "2018-01-21T15:34:19.304356Z", "url": "https://files.pythonhosted.org/packages/4a/88/c28e2a2da8f3dc3a391d9c97ad949f2ea0c05198222e7e6af176e5bf9b26/more_itertools-4.1.0-py2-none-any.whl", "yanked": false }, { "core-metadata": { "sha256": "59c3513ac4a78ad61b2a0eda88807985c4bb71cd36e6917f2f9c0ea121a7c029" }, "data-dist-info-metadata": { "sha256": "59c3513ac4a78ad61b2a0eda88807985c4bb71cd36e6917f2f9c0ea121a7c029" }, "filename": "more_itertools-4.1.0-py3-none-any.whl", "hashes": { "md5": "ae17a45d13e9dc319794c40fa739c38f", "sha256": "580b6002d1f28feb5bcb8303278d59cf17dfbd19a63a5c2375112dae72c9bf98" }, "provenance": null, "requires-python": null, "size": 47988, "upload-time": "2018-01-21T15:34:20.567853Z", "url": "https://files.pythonhosted.org/packages/7a/46/886917c6a4ce49dd3fff250c01c5abac5390d57992751384fe61befc4877/more_itertools-4.1.0-py3-none-any.whl", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "more-itertools", "project-status": { "status": "active" }, "versions": [ "4.1.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pastel/0.1.0.json ================================================ { "info": { "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "https://github.com/sdispater/pastel/archive/0.1.0.tar.gz", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/sdispater/pastel", "keywords": "", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "pastel", "package_url": "https://pypi.org/project/pastel/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/pastel/", "project_urls": { "Download": "https://github.com/sdispater/pastel/archive/0.1.0.tar.gz", "Homepage": "https://github.com/sdispater/pastel" }, "provides_extra": null, "release_url": "https://pypi.org/project/pastel/0.1.0/", "requires_dist": null, "requires_python": "", "summary": "Bring colors to your terminal.", "version": "0.1.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "cf7c53ab0a5d7e7c721425b24b486124", "sha256": "754d192c088e256d52a3f825c3b9e14252d5adc70f53656453f6431e50a70b99" }, "downloads": -1, "filename": "pastel-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "cf7c53ab0a5d7e7c721425b24b486124", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 6718, "upload_time": "2017-02-01T23:26:20", "upload_time_iso_8601": "2017-02-01T23:26:20.148511Z", "url": "https://files.pythonhosted.org/packages/9b/7e/7d701686013c0d7dae62e0977467232a6adc2e562c23878eb3cd4f97d02e/pastel-0.1.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "43ea5f07660f630da18ae1827f5b4333", "sha256": "22f14474c4120b37c54ac2173b49b0ac1de9283ca714be6eb3ea8b39296285a9" }, "downloads": -1, "filename": "pastel-0.1.0.tar.gz", "has_sig": false, "md5_digest": "43ea5f07660f630da18ae1827f5b4333", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 4490, "upload_time": "2017-02-01T23:26:21", "upload_time_iso_8601": "2017-02-01T23:26:21.720111Z", "url": "https://files.pythonhosted.org/packages/bd/13/a68f2e448b471e8c49e9b596d569ae167a5135ac672b1dc5f24f62f9c15f/pastel-0.1.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pastel.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "a3d071a0b389c60bee1f22fa02aea80322ff957ba0704d4c385f48291af2125d" }, "data-dist-info-metadata": { "sha256": "a3d071a0b389c60bee1f22fa02aea80322ff957ba0704d4c385f48291af2125d" }, "filename": "pastel-0.1.0-py3-none-any.whl", "hashes": { "md5": "cf7c53ab0a5d7e7c721425b24b486124", "sha256": "754d192c088e256d52a3f825c3b9e14252d5adc70f53656453f6431e50a70b99" }, "provenance": null, "requires-python": null, "size": 6718, "upload-time": "2017-02-01T23:26:20.148511Z", "url": "https://files.pythonhosted.org/packages/9b/7e/7d701686013c0d7dae62e0977467232a6adc2e562c23878eb3cd4f97d02e/pastel-0.1.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "pastel-0.1.0.tar.gz", "hashes": { "md5": "43ea5f07660f630da18ae1827f5b4333", "sha256": "22f14474c4120b37c54ac2173b49b0ac1de9283ca714be6eb3ea8b39296285a9" }, "provenance": null, "requires-python": null, "size": 4490, "upload-time": "2017-02-01T23:26:21.720111Z", "url": "https://files.pythonhosted.org/packages/bd/13/a68f2e448b471e8c49e9b596d569ae167a5135ac672b1dc5f24f62f9c15f/pastel-0.1.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "pastel", "project-status": { "status": "active" }, "versions": [ "0.1.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pluggy/0.6.0.json ================================================ { "info": { "author": "Holger Krekel", "author_email": "holger@merlinux.eu", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", "Topic :: Utilities" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/pytest-dev/pluggy", "keywords": "", "license": "MIT license", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "pluggy", "package_url": "https://pypi.org/project/pluggy/", "platform": "unix", "project_url": "https://pypi.org/project/pluggy/", "project_urls": { "Homepage": "https://github.com/pytest-dev/pluggy" }, "provides_extra": null, "release_url": "https://pypi.org/project/pluggy/0.6.0/", "requires_dist": null, "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "plugin and hook calling mechanisms for python", "version": "0.6.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "095eed084713c9b2a9a01520485e20fb", "sha256": "f5f767d398f18aa177976bf9c4d0c05d96487a7d8f07062251585803aaf56246" }, "downloads": -1, "filename": "pluggy-0.6.0-py2-none-any.whl", "has_sig": false, "md5_digest": "095eed084713c9b2a9a01520485e20fb", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 11953, "upload_time": "2018-04-15T17:55:28", "upload_time_iso_8601": "2018-04-15T17:55:28.983278Z", "url": "https://files.pythonhosted.org/packages/82/05/43e3947125a2137cba4746135c75934ceed1863f27e050fc560052104a71/pluggy-0.6.0-py2-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "2b6dc266f54023dfb26726686ee6b227", "sha256": "d34798b80853ab688de1a3ca5b99ba4de91c459c19c76a555dc939979ae67eb0" }, "downloads": -1, "filename": "pluggy-0.6.0-py3-none-any.whl", "has_sig": false, "md5_digest": "2b6dc266f54023dfb26726686ee6b227", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 13723, "upload_time": "2018-04-15T17:55:22", "upload_time_iso_8601": "2018-04-15T17:55:22.927924Z", "url": "https://files.pythonhosted.org/packages/ba/65/ded3bc40bbf8d887f262f150fbe1ae6637765b5c9534bd55690ed2c0b0f7/pluggy-0.6.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "ef8a88abcd501afd47cb22245fe4315a", "sha256": "a982e208d054867661d27c6d2a86b17ba05fbb6b1bdc01f42660732dd107f865" }, "downloads": -1, "filename": "pluggy-0.6.0.tar.gz", "has_sig": false, "md5_digest": "ef8a88abcd501afd47cb22245fe4315a", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 19678, "upload_time": "2017-11-24T16:33:11", "upload_time_iso_8601": "2017-11-24T16:33:11.250495Z", "url": "https://files.pythonhosted.org/packages/11/bf/cbeb8cdfaffa9f2ea154a30ae31a9d04a1209312e2919138b4171a1f8199/pluggy-0.6.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pluggy.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "4d9f559e9a7cda8e28dcedbc0f37ef6335a7560e3386c8016358ed4c5f3785a6" }, "data-dist-info-metadata": { "sha256": "4d9f559e9a7cda8e28dcedbc0f37ef6335a7560e3386c8016358ed4c5f3785a6" }, "filename": "pluggy-0.6.0-py2-none-any.whl", "hashes": { "md5": "095eed084713c9b2a9a01520485e20fb", "sha256": "f5f767d398f18aa177976bf9c4d0c05d96487a7d8f07062251585803aaf56246" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 11953, "upload-time": "2018-04-15T17:55:28.983278Z", "url": "https://files.pythonhosted.org/packages/82/05/43e3947125a2137cba4746135c75934ceed1863f27e050fc560052104a71/pluggy-0.6.0-py2-none-any.whl", "yanked": false }, { "core-metadata": { "sha256": "77f2f50376e7e82c6302a126af0eaf929596851f0586ced7ac1d5d381c9b6f19" }, "data-dist-info-metadata": { "sha256": "77f2f50376e7e82c6302a126af0eaf929596851f0586ced7ac1d5d381c9b6f19" }, "filename": "pluggy-0.6.0-py3-none-any.whl", "hashes": { "md5": "2b6dc266f54023dfb26726686ee6b227", "sha256": "d34798b80853ab688de1a3ca5b99ba4de91c459c19c76a555dc939979ae67eb0" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 13723, "upload-time": "2018-04-15T17:55:22.927924Z", "url": "https://files.pythonhosted.org/packages/ba/65/ded3bc40bbf8d887f262f150fbe1ae6637765b5c9534bd55690ed2c0b0f7/pluggy-0.6.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "pluggy-0.6.0.tar.gz", "hashes": { "md5": "ef8a88abcd501afd47cb22245fe4315a", "sha256": "a982e208d054867661d27c6d2a86b17ba05fbb6b1bdc01f42660732dd107f865" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 19678, "upload-time": "2017-11-24T16:33:11.250495Z", "url": "https://files.pythonhosted.org/packages/11/bf/cbeb8cdfaffa9f2ea154a30ae31a9d04a1209312e2919138b4171a1f8199/pluggy-0.6.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "pluggy", "project-status": { "status": "active" }, "versions": [ "0.6.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/poetry-core/1.5.0.json ================================================ { "info": { "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/python-poetry/poetry-core", "keywords": "packaging,dependency,poetry", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "poetry-core", "package_url": "https://pypi.org/project/poetry-core/", "platform": null, "project_url": "https://pypi.org/project/poetry-core/", "project_urls": { "Bug Tracker": "https://github.com/python-poetry/poetry/issues", "Homepage": "https://github.com/python-poetry/poetry-core", "Repository": "https://github.com/python-poetry/poetry-core" }, "provides_extra": null, "release_url": "https://pypi.org/project/poetry-core/1.5.0/", "requires_dist": [ "importlib-metadata (>=1.7.0) ; python_version < \"3.8\"" ], "requires_python": ">=3.7,<4.0", "summary": "Poetry PEP 517 Build Backend", "version": "1.5.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "be7589b4902793e66d7d979bd8581591", "sha256": "e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84" }, "downloads": -1, "filename": "poetry_core-1.5.0-py3-none-any.whl", "has_sig": false, "md5_digest": "be7589b4902793e66d7d979bd8581591", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7,<4.0", "size": 464992, "upload_time": "2023-01-28T10:52:52", "upload_time_iso_8601": "2023-01-28T10:52:52.445537Z", "url": "https://files.pythonhosted.org/packages/2d/99/6b0c5fe90e247b2b7b96a27cdf39ee59a02aab3c01d7243fc0c63cd7fb73/poetry_core-1.5.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "3f9b36a7a94cd235bfd5f05794828445", "sha256": "0ae8d28caf5c12ec1714b16d2e7157ddd52397ea6bfdeba5a9432e449a0184da" }, "downloads": -1, "filename": "poetry_core-1.5.0.tar.gz", "has_sig": false, "md5_digest": "3f9b36a7a94cd235bfd5f05794828445", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7,<4.0", "size": 448812, "upload_time": "2023-01-28T10:52:53", "upload_time_iso_8601": "2023-01-28T10:52:53.916268Z", "url": "https://files.pythonhosted.org/packages/57/bb/2435fef60bb01f6c0891d9482c7053b50e90639f0f74d7658e99bdd4da69/poetry_core-1.5.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/poetry-core/2.0.1.json ================================================ { "info": { "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": null, "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": null, "keywords": "packaging, dependency, poetry", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "Arun Babu Neelicattu", "maintainer_email": "arun.neelicattu@gmail.com", "name": "poetry-core", "package_url": "https://pypi.org/project/poetry-core/", "platform": null, "project_url": "https://pypi.org/project/poetry-core/", "project_urls": { "Bug Tracker": "https://github.com/python-poetry/poetry/issues", "Homepage": "https://github.com/python-poetry/poetry-core", "Repository": "https://github.com/python-poetry/poetry-core" }, "provides_extra": null, "release_url": "https://pypi.org/project/poetry-core/2.0.1/", "requires_dist": null, "requires_python": "<4.0,>=3.9", "summary": "Poetry PEP 517 Build Backend", "version": "2.0.1", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "a52cf4beef0de009e0a9a36c9e6962f5", "sha256": "a3c7009536522cda4eb0fb3805c9dc935b5537f8727dd01efb9c15e51a17552b" }, "downloads": -1, "filename": "poetry_core-2.0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "a52cf4beef0de009e0a9a36c9e6962f5", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": "<4.0,>=3.9", "size": 544761, "upload_time": "2025-01-11T18:34:58", "upload_time_iso_8601": "2025-01-11T18:34:58.051401Z", "url": "https://files.pythonhosted.org/packages/99/9e/b2d13aecfd3a7ea05e6eeddff7c450b8ff5233e5d0da834fbfd81b19acaf/poetry_core-2.0.1-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "1b1bb959cd760ac509de9b38ae67fc3b", "sha256": "d2acdaec3b93dc1ab43adaeb0e9a8a6a6b3701c4535b5baab4b718ab12c8993c" }, "downloads": -1, "filename": "poetry_core-2.0.1.tar.gz", "has_sig": false, "md5_digest": "1b1bb959cd760ac509de9b38ae67fc3b", "packagetype": "sdist", "python_version": "source", "requires_python": "<4.0,>=3.9", "size": 355487, "upload_time": "2025-01-11T18:35:01", "upload_time_iso_8601": "2025-01-11T18:35:01.191422Z", "url": "https://files.pythonhosted.org/packages/c4/f5/89d11008714e0a49cab9cba7cce89c66ea5a94f37cc6d283798cc1725fac/poetry_core-2.0.1.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/poetry-core.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "7901e9c07999287d28be06df377c9e8529ec602c583dc7e2f22c1211d5f747e3" }, "data-dist-info-metadata": { "sha256": "7901e9c07999287d28be06df377c9e8529ec602c583dc7e2f22c1211d5f747e3" }, "filename": "poetry_core-1.5.0-py3-none-any.whl", "hashes": { "md5": "be7589b4902793e66d7d979bd8581591", "sha256": "e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84" }, "provenance": null, "requires-python": ">=3.7,<4.0", "size": 464992, "upload-time": "2023-01-28T10:52:52.445537Z", "url": "https://files.pythonhosted.org/packages/2d/99/6b0c5fe90e247b2b7b96a27cdf39ee59a02aab3c01d7243fc0c63cd7fb73/poetry_core-1.5.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry_core-1.5.0.tar.gz", "hashes": { "md5": "3f9b36a7a94cd235bfd5f05794828445", "sha256": "0ae8d28caf5c12ec1714b16d2e7157ddd52397ea6bfdeba5a9432e449a0184da" }, "provenance": null, "requires-python": ">=3.7,<4.0", "size": 448812, "upload-time": "2023-01-28T10:52:53.916268Z", "url": "https://files.pythonhosted.org/packages/57/bb/2435fef60bb01f6c0891d9482c7053b50e90639f0f74d7658e99bdd4da69/poetry_core-1.5.0.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "61e9048f1aa5ec95b9327f64f2d2618b80b4564f0b406a8a18980a36f5d9fc4a" }, "data-dist-info-metadata": { "sha256": "61e9048f1aa5ec95b9327f64f2d2618b80b4564f0b406a8a18980a36f5d9fc4a" }, "filename": "poetry_core-2.0.1-py3-none-any.whl", "hashes": { "md5": "a52cf4beef0de009e0a9a36c9e6962f5", "sha256": "a3c7009536522cda4eb0fb3805c9dc935b5537f8727dd01efb9c15e51a17552b" }, "provenance": "https://pypi.org/integrity/poetry-core/2.0.1/poetry_core-2.0.1-py3-none-any.whl/provenance", "requires-python": "<4.0,>=3.9", "size": 544761, "upload-time": "2025-01-11T18:34:58.051401Z", "url": "https://files.pythonhosted.org/packages/99/9e/b2d13aecfd3a7ea05e6eeddff7c450b8ff5233e5d0da834fbfd81b19acaf/poetry_core-2.0.1-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "poetry_core-2.0.1.tar.gz", "hashes": { "md5": "1b1bb959cd760ac509de9b38ae67fc3b", "sha256": "d2acdaec3b93dc1ab43adaeb0e9a8a6a6b3701c4535b5baab4b718ab12c8993c" }, "provenance": "https://pypi.org/integrity/poetry-core/2.0.1/poetry_core-2.0.1.tar.gz/provenance", "requires-python": "<4.0,>=3.9", "size": 355487, "upload-time": "2025-01-11T18:35:01.191422Z", "url": "https://files.pythonhosted.org/packages/c4/f5/89d11008714e0a49cab9cba7cce89c66ea5a94f37cc6d283798cc1725fac/poetry_core-2.0.1.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "poetry-core", "project-status": { "status": "active" }, "versions": [ "1.5.0", "2.0.1" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/py/1.5.3.json ================================================ { "info": { "author": "holger krekel, Ronny Pfannschmidt, Benjamin Peterson and others", "author_email": "pytest-dev@python.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 6 - Mature", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", "Topic :: Utilities" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://py.readthedocs.io/", "keywords": "", "license": "MIT license", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "py", "package_url": "https://pypi.org/project/py/", "platform": "unix", "project_url": "https://pypi.org/project/py/", "project_urls": { "Homepage": "http://py.readthedocs.io/" }, "provides_extra": null, "release_url": "https://pypi.org/project/py/1.5.3/", "requires_dist": null, "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "library with cross-python path, ini-parsing, io, code, log facilities", "version": "1.5.3", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "b316b380701661cb67732ecdaef30eeb", "sha256": "ef4a94f47156178e42ef8f2b131db420e0f4b6aa0b3936b6dbde6ad6487476a5" }, "downloads": -1, "filename": "py-1.5.3-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "b316b380701661cb67732ecdaef30eeb", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 84903, "upload_time": "2018-03-22T10:06:50", "upload_time_iso_8601": "2018-03-22T10:06:50.318783Z", "url": "https://files.pythonhosted.org/packages/67/a5/f77982214dd4c8fd104b066f249adea2c49e25e8703d284382eb5e9ab35a/py-1.5.3-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "623e80cfc06df930414a9ce4bf0fd6c9", "sha256": "2df2c513c3af11de15f58189ba5539ddc4768c6f33816dc5c03950c8bd6180fa" }, "downloads": -1, "filename": "py-1.5.3.tar.gz", "has_sig": false, "md5_digest": "623e80cfc06df930414a9ce4bf0fd6c9", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 202335, "upload_time": "2018-03-22T10:06:52", "upload_time_iso_8601": "2018-03-22T10:06:52.627078Z", "url": "https://files.pythonhosted.org/packages/f7/84/b4c6e84672c4ceb94f727f3da8344037b62cee960d80e999b1cd9b832d83/py-1.5.3.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/py.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "6922e816d9a839c9735c50ad24e50bdc0e42c3727cf742f5121e3d6bf58d79e2" }, "data-dist-info-metadata": { "sha256": "6922e816d9a839c9735c50ad24e50bdc0e42c3727cf742f5121e3d6bf58d79e2" }, "filename": "py-1.5.3-py2.py3-none-any.whl", "hashes": { "md5": "b316b380701661cb67732ecdaef30eeb", "sha256": "ef4a94f47156178e42ef8f2b131db420e0f4b6aa0b3936b6dbde6ad6487476a5" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 84903, "upload-time": "2018-03-22T10:06:50.318783Z", "url": "https://files.pythonhosted.org/packages/67/a5/f77982214dd4c8fd104b066f249adea2c49e25e8703d284382eb5e9ab35a/py-1.5.3-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "py-1.5.3.tar.gz", "hashes": { "md5": "623e80cfc06df930414a9ce4bf0fd6c9", "sha256": "2df2c513c3af11de15f58189ba5539ddc4768c6f33816dc5c03950c8bd6180fa" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 202335, "upload-time": "2018-03-22T10:06:52.627078Z", "url": "https://files.pythonhosted.org/packages/f7/84/b4c6e84672c4ceb94f727f3da8344037b62cee960d80e999b1cd9b832d83/py-1.5.3.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "py", "project-status": { "status": "active" }, "versions": [ "1.5.3" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pylev/1.3.0.json ================================================ { "info": { "author": "Daniel Lindsley", "author_email": "daniel@toastdriven.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://github.com/toastdriven/pylev", "keywords": null, "license": "UNKNOWN", "license_expression": null, "license_files": null, "maintainer": null, "maintainer_email": null, "name": "pylev", "package_url": "https://pypi.org/project/pylev/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/pylev/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://github.com/toastdriven/pylev" }, "provides_extra": null, "release_url": "https://pypi.org/project/pylev/1.3.0/", "requires_dist": null, "requires_python": null, "summary": "A pure Python Levenshtein implementation that's not freaking GPL'd.", "version": "1.3.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "401c7dff1d242bf1e19f9c6202f0ba4e6fd18cc7ecb8bc85b17b2d16c806e228", "md5": "6da14dfce5034873fc5c2d7a6e83dc29", "sha256": "1d29a87beb45ebe1e821e7a3b10da2b6b2f4c79b43f482c2df1a1f748a6e114e" }, "downloads": -1, "filename": "pylev-1.3.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "6da14dfce5034873fc5c2d7a6e83dc29", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 4927, "upload_time": "2014-10-23T00:24:34", "upload_time_iso_8601": "2014-10-23T00:24:34.125905Z", "url": "https://files.pythonhosted.org/packages/40/1c/7dff1d242bf1e19f9c6202f0ba4e6fd18cc7ecb8bc85b17b2d16c806e228/pylev-1.3.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "cc61dab2081d3d86dcf0b9f5dcfb11b256d76cd14aad7ccdd7c8dd5e7f7e41a0", "md5": "3be579cfc32ce5140cc04001f898741b", "sha256": "063910098161199b81e453025653ec53556c1be7165a9b7c50be2f4d57eae1c3" }, "downloads": -1, "filename": "pylev-1.3.0.tar.gz", "has_sig": false, "md5_digest": "3be579cfc32ce5140cc04001f898741b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 3193, "upload_time": "2014-10-23T00:24:19", "upload_time_iso_8601": "2014-10-23T00:24:19.460779Z", "url": "https://files.pythonhosted.org/packages/cc/61/dab2081d3d86dcf0b9f5dcfb11b256d76cd14aad7ccdd7c8dd5e7f7e41a0/pylev-1.3.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pylev.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "4a5b8c2be7e6f09ffe9a9b8018091c79074f61601fbbb693d6a3bd93bb4d5242" }, "data-dist-info-metadata": { "sha256": "4a5b8c2be7e6f09ffe9a9b8018091c79074f61601fbbb693d6a3bd93bb4d5242" }, "filename": "pylev-1.3.0-py2.py3-none-any.whl", "hashes": { "sha256": "1d29a87beb45ebe1e821e7a3b10da2b6b2f4c79b43f482c2df1a1f748a6e114e" }, "provenance": null, "requires-python": null, "size": 4927, "upload-time": "2014-10-23T00:24:34.125905Z", "url": "https://files.pythonhosted.org/packages/40/1c/7dff1d242bf1e19f9c6202f0ba4e6fd18cc7ecb8bc85b17b2d16c806e228/pylev-1.3.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "pylev-1.3.0.tar.gz", "hashes": { "sha256": "063910098161199b81e453025653ec53556c1be7165a9b7c50be2f4d57eae1c3" }, "provenance": null, "requires-python": null, "size": 3193, "upload-time": "2014-10-23T00:24:19.460779Z", "url": "https://files.pythonhosted.org/packages/cc/61/dab2081d3d86dcf0b9f5dcfb11b256d76cd14aad7ccdd7c8dd5e7f7e41a0/pylev-1.3.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "pylev", "project-status": { "status": "active" }, "versions": [ "1.3.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pytest/3.5.0.json ================================================ { "info": { "author": "Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others", "author_email": "", "bugtrack_url": null, "classifiers": [ "Development Status :: 6 - Mature", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", "Topic :: Utilities" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pytest.org", "keywords": "test unittest", "license": "MIT license", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "pytest", "package_url": "https://pypi.org/project/pytest/", "platform": "unix", "project_url": "https://pypi.org/project/pytest/", "project_urls": { "Homepage": "http://pytest.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/pytest/3.5.0/", "requires_dist": [ "py (>=1.5.0)", "six (>=1.10.0)", "setuptools", "attrs (>=17.4.0)", "more-itertools (>=4.0.0)", "pluggy (<0.7,>=0.5)", "funcsigs; python_version < \"3.0\"", "colorama; sys_platform == \"win32\"" ], "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "pytest: simple powerful testing with Python", "version": "3.5.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "4a8651dec151e76f283bf59e333286f9", "sha256": "427b4582bda18e92ad1967e8b1e071e2c53e6cb7e3e5f090cb3ca443455be23f" }, "downloads": -1, "filename": "pytest-3.5.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "4a8651dec151e76f283bf59e333286f9", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 194247, "upload_time": "2018-03-22T23:47:54", "upload_time_iso_8601": "2018-03-22T23:47:54.595523Z", "url": "https://files.pythonhosted.org/packages/ed/96/271c93f75212c06e2a7ec3e2fa8a9c90acee0a4838dc05bf379ea09aae31/pytest-3.5.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "ccd78dac54112045f561c4df86631f19", "sha256": "677b1d6decd29c041fe64276f29f79fbe66e40c59e445eb251366b4a8ab8bf68" }, "downloads": -1, "filename": "pytest-3.5.0.tar.gz", "has_sig": false, "md5_digest": "ccd78dac54112045f561c4df86631f19", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 830816, "upload_time": "2018-03-22T23:47:56", "upload_time_iso_8601": "2018-03-22T23:47:56.511852Z", "url": "https://files.pythonhosted.org/packages/2d/56/6019153cdd743300c5688ab3b07702355283e53c83fbf922242c053ffb7b/pytest-3.5.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pytest/3.5.1.json ================================================ { "info": { "author": "Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others", "author_email": "", "bugtrack_url": null, "classifiers": [ "Development Status :: 6 - Mature", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", "Topic :: Utilities" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pytest.org", "keywords": "test unittest", "license": "MIT license", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "pytest", "package_url": "https://pypi.org/project/pytest/", "platform": "unix", "project_url": "https://pypi.org/project/pytest/", "project_urls": { "Homepage": "http://pytest.org", "Source": "https://github.com/pytest-dev/pytest", "Tracker": "https://github.com/pytest-dev/pytest/issues" }, "provides_extra": null, "release_url": "https://pypi.org/project/pytest/3.5.1/", "requires_dist": [ "py (>=1.5.0)", "six (>=1.10.0)", "setuptools", "attrs (>=17.4.0)", "more-itertools (>=4.0.0)", "pluggy (<0.7,>=0.5)", "funcsigs; python_version < \"3.0\"", "colorama; sys_platform == \"win32\"" ], "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "pytest: simple powerful testing with Python", "version": "3.5.1", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "1e81fba94885bef80170545d045924eb", "sha256": "d327df3686046c5b374a9776d9e11606f7dba6fb3db5cf5d60ebc78a31e0768e" }, "downloads": -1, "filename": "pytest-3.5.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "1e81fba94885bef80170545d045924eb", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 192143, "upload_time": "2018-04-24T21:37:43", "upload_time_iso_8601": "2018-04-24T21:37:43.104462Z", "url": "https://files.pythonhosted.org/packages/76/52/fc48d02492d9e6070cb672d9133382e83084f567f88eff1c27bd2c6c27a8/pytest-3.5.1-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "961104636090457187851ccb9ef0f677", "sha256": "b8fe151f3e181801dd38583a1c03818fbc662a8fce96c9063a0af624613e78f8" }, "downloads": -1, "filename": "pytest-3.5.1.tar.gz", "has_sig": false, "md5_digest": "961104636090457187851ccb9ef0f677", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 830571, "upload_time": "2018-04-24T21:37:44", "upload_time_iso_8601": "2018-04-24T21:37:44.492084Z", "url": "https://files.pythonhosted.org/packages/b2/85/24954df0ea8156599563b753de54383a5d702081093b7953334e4701b8d8/pytest-3.5.1.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pytest.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "3825a201ef8accd23ae8e728ac48c8f06fdf04cbdf2aa5f0729cbcab5d06a833" }, "data-dist-info-metadata": { "sha256": "3825a201ef8accd23ae8e728ac48c8f06fdf04cbdf2aa5f0729cbcab5d06a833" }, "filename": "pytest-3.5.0-py2.py3-none-any.whl", "hashes": { "md5": "4a8651dec151e76f283bf59e333286f9", "sha256": "427b4582bda18e92ad1967e8b1e071e2c53e6cb7e3e5f090cb3ca443455be23f" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 194247, "upload-time": "2018-03-22T23:47:54.595523Z", "url": "https://files.pythonhosted.org/packages/ed/96/271c93f75212c06e2a7ec3e2fa8a9c90acee0a4838dc05bf379ea09aae31/pytest-3.5.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "pytest-3.5.0.tar.gz", "hashes": { "md5": "ccd78dac54112045f561c4df86631f19", "sha256": "677b1d6decd29c041fe64276f29f79fbe66e40c59e445eb251366b4a8ab8bf68" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 830816, "upload-time": "2018-03-22T23:47:56.511852Z", "url": "https://files.pythonhosted.org/packages/2d/56/6019153cdd743300c5688ab3b07702355283e53c83fbf922242c053ffb7b/pytest-3.5.0.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "8a80e51c143666a2d213dc466ec5be4ab558c6d84457173f4ed079f4331ebb71" }, "data-dist-info-metadata": { "sha256": "8a80e51c143666a2d213dc466ec5be4ab558c6d84457173f4ed079f4331ebb71" }, "filename": "pytest-3.5.1-py2.py3-none-any.whl", "hashes": { "md5": "1e81fba94885bef80170545d045924eb", "sha256": "d327df3686046c5b374a9776d9e11606f7dba6fb3db5cf5d60ebc78a31e0768e" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 192143, "upload-time": "2018-04-24T21:37:43.104462Z", "url": "https://files.pythonhosted.org/packages/76/52/fc48d02492d9e6070cb672d9133382e83084f567f88eff1c27bd2c6c27a8/pytest-3.5.1-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "pytest-3.5.1.tar.gz", "hashes": { "md5": "961104636090457187851ccb9ef0f677", "sha256": "b8fe151f3e181801dd38583a1c03818fbc662a8fce96c9063a0af624613e78f8" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 830571, "upload-time": "2018-04-24T21:37:44.492084Z", "url": "https://files.pythonhosted.org/packages/b2/85/24954df0ea8156599563b753de54383a5d702081093b7953334e4701b8d8/pytest-3.5.1.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "pytest", "project-status": { "status": "active" }, "versions": [ "3.5.0", "3.5.1" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/python-language-server/0.21.2.json ================================================ { "info": { "author": "Palantir Technologies, Inc.", "author_email": "", "bugtrack_url": null, "classifiers": [], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/palantir/python-language-server", "keywords": "", "license": "", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "python-language-server", "package_url": "https://pypi.org/project/python-language-server/", "platform": "", "project_url": "https://pypi.org/project/python-language-server/", "project_urls": { "Homepage": "https://github.com/palantir/python-language-server" }, "provides_extra": null, "release_url": "https://pypi.org/project/python-language-server/0.21.2/", "requires_dist": null, "requires_python": "", "summary": "Python Language Server for the Language Server Protocol", "version": "0.21.2", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "677602ec38bc1c7b72de6128d90d846b", "sha256": "91b564e092f3135b2bac70dbd23d283da5ad50269766a76648787b69fe702c7e" }, "downloads": -1, "filename": "python-language-server-0.21.2.tar.gz", "has_sig": false, "md5_digest": "677602ec38bc1c7b72de6128d90d846b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 51676, "upload_time": "2018-09-06T18:11:28", "upload_time_iso_8601": "2018-09-06T18:11:28.546667Z", "url": "https://files.pythonhosted.org/packages/9f/1d/2817b5dc2dd77f897410a11c1c9e2a6d96b3273c53d4219dd9edab7882af/python-language-server-0.21.2.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/python-language-server.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "python-language-server-0.21.2.tar.gz", "hashes": { "md5": "677602ec38bc1c7b72de6128d90d846b", "sha256": "91b564e092f3135b2bac70dbd23d283da5ad50269766a76648787b69fe702c7e" }, "provenance": null, "requires-python": null, "size": 51676, "upload-time": "2018-09-06T18:11:28.546667Z", "url": "https://files.pythonhosted.org/packages/9f/1d/2817b5dc2dd77f897410a11c1c9e2a6d96b3273c53d4219dd9edab7882af/python-language-server-0.21.2.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "python-language-server", "project-status": { "status": "active" }, "versions": [ "0.21.2" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pyyaml/3.13.0.json ================================================ { "info": { "author": "Kirill Simonov", "author_email": "xi@resolvent.net", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: Markup" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pyyaml.org/wiki/PyYAML", "keywords": "", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "PyYAML", "package_url": "https://pypi.org/project/PyYAML/", "platform": "Any", "project_url": "https://pypi.org/project/PyYAML/", "project_urls": { "Download": "http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz", "Homepage": "http://pyyaml.org/wiki/PyYAML" }, "provides_extra": null, "release_url": "https://pypi.org/project/PyYAML/3.13/", "requires_dist": null, "requires_python": "", "summary": "YAML parser and emitter for Python", "version": "3.13", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "b82e9c2285870c9de070a1fa5ede702ab5fb329901b3cc4028c24f44eda27c5f", "md5": "a83441aa7004e474bed6f6daeb61f27a", "sha256": "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f" }, "downloads": -1, "filename": "PyYAML-3.13-cp27-cp27m-win32.whl", "has_sig": false, "md5_digest": "a83441aa7004e474bed6f6daeb61f27a", "packagetype": "bdist_wheel", "python_version": "cp27", "requires_python": null, "size": 191712, "upload_time": "2018-07-05T22:53:15", "upload_time_iso_8601": "2018-07-05T22:53:15.231061Z", "url": "https://files.pythonhosted.org/packages/b8/2e/9c2285870c9de070a1fa5ede702ab5fb329901b3cc4028c24f44eda27c5f/PyYAML-3.13-cp27-cp27m-win32.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "df4d1ef8d60464a171112401e17a3a3e88fdb1d5b44af7606e8652b2f39ee9ce", "md5": "dd05ba2d6cb042452a3849dea13b94f0", "sha256": "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537" }, "downloads": -1, "filename": "PyYAML-3.13-cp27-cp27m-win_amd64.whl", "has_sig": false, "md5_digest": "dd05ba2d6cb042452a3849dea13b94f0", "packagetype": "bdist_wheel", "python_version": "cp27", "requires_python": null, "size": 209872, "upload_time": "2018-07-05T22:53:16", "upload_time_iso_8601": "2018-07-05T22:53:16.904443Z", "url": "https://files.pythonhosted.org/packages/df/4d/1ef8d60464a171112401e17a3a3e88fdb1d5b44af7606e8652b2f39ee9ce/PyYAML-3.13-cp27-cp27m-win_amd64.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "35f0cf0363b5c431c3a828284903aeacc6bdbba342fd4d7871dda9a3b0b00d15", "md5": "49365caa070d53e30deceae118e4fea8", "sha256": "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3" }, "downloads": -1, "filename": "PyYAML-3.13-cp34-cp34m-win32.whl", "has_sig": false, "md5_digest": "49365caa070d53e30deceae118e4fea8", "packagetype": "bdist_wheel", "python_version": "cp34", "requires_python": null, "size": 192898, "upload_time": "2018-07-05T22:53:19", "upload_time_iso_8601": "2018-07-05T22:53:19.190872Z", "url": "https://files.pythonhosted.org/packages/35/f0/cf0363b5c431c3a828284903aeacc6bdbba342fd4d7871dda9a3b0b00d15/PyYAML-3.13-cp34-cp34m-win32.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "8cbc8950092a86259dc511e02a4c3a517ed4b28a254e4da134e3c04e5264e5a3", "md5": "0c486a54c19dd18b9e65a559886935c4", "sha256": "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04" }, "downloads": -1, "filename": "PyYAML-3.13-cp34-cp34m-win_amd64.whl", "has_sig": false, "md5_digest": "0c486a54c19dd18b9e65a559886935c4", "packagetype": "bdist_wheel", "python_version": "cp34", "requires_python": null, "size": 206242, "upload_time": "2018-07-05T22:53:20", "upload_time_iso_8601": "2018-07-05T22:53:20.770605Z", "url": "https://files.pythonhosted.org/packages/8c/bc/8950092a86259dc511e02a4c3a517ed4b28a254e4da134e3c04e5264e5a3/PyYAML-3.13-cp34-cp34m-win_amd64.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "29338bbcd3740d9e96cfb57427b8db7a12093402a3a83f2054887e027b2849de", "md5": "53ce2b9f6b741fb2f070d12839b5789e", "sha256": "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1" }, "downloads": -1, "filename": "PyYAML-3.13-cp35-cp35m-win32.whl", "has_sig": false, "md5_digest": "53ce2b9f6b741fb2f070d12839b5789e", "packagetype": "bdist_wheel", "python_version": "cp35", "requires_python": null, "size": 187499, "upload_time": "2018-07-05T22:53:22", "upload_time_iso_8601": "2018-07-05T22:53:22.576919Z", "url": "https://files.pythonhosted.org/packages/29/33/8bbcd3740d9e96cfb57427b8db7a12093402a3a83f2054887e027b2849de/PyYAML-3.13-cp35-cp35m-win32.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "add4d895fb7ac1b0828151b829a32cefc8a8b58b4499570520b91af20982b880", "md5": "1b70e7ced4c82364bda4ac9094d6e259", "sha256": "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613" }, "downloads": -1, "filename": "PyYAML-3.13-cp35-cp35m-win_amd64.whl", "has_sig": false, "md5_digest": "1b70e7ced4c82364bda4ac9094d6e259", "packagetype": "bdist_wheel", "python_version": "cp35", "requires_python": null, "size": 205387, "upload_time": "2018-07-05T22:53:24", "upload_time_iso_8601": "2018-07-05T22:53:24.438646Z", "url": "https://files.pythonhosted.org/packages/ad/d4/d895fb7ac1b0828151b829a32cefc8a8b58b4499570520b91af20982b880/PyYAML-3.13-cp35-cp35m-win_amd64.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "fb510c49c6caafe8d9a27ad9b0ca9f91adda5a5072b9efbbe7585fb97a4c71c4", "md5": "8f62197b853b5b387ff588df05cee7a6", "sha256": "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a" }, "downloads": -1, "filename": "PyYAML-3.13-cp36-cp36m-win32.whl", "has_sig": false, "md5_digest": "8f62197b853b5b387ff588df05cee7a6", "packagetype": "bdist_wheel", "python_version": "cp36", "requires_python": null, "size": 188186, "upload_time": "2018-07-05T22:53:25", "upload_time_iso_8601": "2018-07-05T22:53:25.923669Z", "url": "https://files.pythonhosted.org/packages/fb/51/0c49c6caafe8d9a27ad9b0ca9f91adda5a5072b9efbbe7585fb97a4c71c4/PyYAML-3.13-cp36-cp36m-win32.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "4fca5fad249c5032270540c24d2189b0ddf1396aac49b0bdc548162edcf14131", "md5": "ff7280dd032d202b417871d39febadec", "sha256": "3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b" }, "downloads": -1, "filename": "PyYAML-3.13-cp36-cp36m-win_amd64.whl", "has_sig": false, "md5_digest": "ff7280dd032d202b417871d39febadec", "packagetype": "bdist_wheel", "python_version": "cp36", "requires_python": null, "size": 206277, "upload_time": "2018-07-05T22:53:27", "upload_time_iso_8601": "2018-07-05T22:53:27.386610Z", "url": "https://files.pythonhosted.org/packages/4f/ca/5fad249c5032270540c24d2189b0ddf1396aac49b0bdc548162edcf14131/PyYAML-3.13-cp36-cp36m-win_amd64.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "5cedd6557f70daaaab6ee5cd2f8ccf7bedd63081e522e38679c03840e1acc114", "md5": "03ac720a2dcb18f2f1a3d026d281d778", "sha256": "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" }, "downloads": -1, "filename": "PyYAML-3.13-cp37-cp37m-win32.whl", "has_sig": false, "md5_digest": "03ac720a2dcb18f2f1a3d026d281d778", "packagetype": "bdist_wheel", "python_version": "cp37", "requires_python": null, "size": 188313, "upload_time": "2018-07-05T22:53:28", "upload_time_iso_8601": "2018-07-05T22:53:28.995194Z", "url": "https://files.pythonhosted.org/packages/5c/ed/d6557f70daaaab6ee5cd2f8ccf7bedd63081e522e38679c03840e1acc114/PyYAML-3.13-cp37-cp37m-win32.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "bf96d02ef8e1f3073e07ffdc240444e5041f403f29c0775f9f1653f18221082f", "md5": "02ab28701247a80e059daa6efe11e67d", "sha256": "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1" }, "downloads": -1, "filename": "PyYAML-3.13-cp37-cp37m-win_amd64.whl", "has_sig": false, "md5_digest": "02ab28701247a80e059daa6efe11e67d", "packagetype": "bdist_wheel", "python_version": "cp37", "requires_python": null, "size": 206614, "upload_time": "2018-07-05T22:53:30", "upload_time_iso_8601": "2018-07-05T22:53:30.864210Z", "url": "https://files.pythonhosted.org/packages/bf/96/d02ef8e1f3073e07ffdc240444e5041f403f29c0775f9f1653f18221082f/PyYAML-3.13-cp37-cp37m-win_amd64.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "9ea31d13970c3f36777c583f136c136f804d70f500168edc1edea6daa7200769", "md5": "b78b96636d68ac581c0e2f38158c224f", "sha256": "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf" }, "downloads": -1, "filename": "PyYAML-3.13.tar.gz", "has_sig": false, "md5_digest": "b78b96636d68ac581c0e2f38158c224f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 270607, "upload_time": "2018-07-05T22:52:16", "upload_time_iso_8601": "2018-07-05T22:52:16.800539Z", "url": "https://files.pythonhosted.org/packages/9e/a3/1d13970c3f36777c583f136c136f804d70f500168edc1edea6daa7200769/PyYAML-3.13.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/pyyaml.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "eb5621acdfd1a084efcc04d1a5292117422c616dc2ea004d8e760aadbd3f8388" }, "data-dist-info-metadata": { "sha256": "eb5621acdfd1a084efcc04d1a5292117422c616dc2ea004d8e760aadbd3f8388" }, "filename": "PyYAML-3.13-cp27-cp27m-win32.whl", "hashes": { "sha256": "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f" }, "provenance": null, "requires-python": null, "size": 191712, "upload-time": "2018-07-05T22:53:15.231061Z", "url": "https://files.pythonhosted.org/packages/b8/2e/9c2285870c9de070a1fa5ede702ab5fb329901b3cc4028c24f44eda27c5f/PyYAML-3.13-cp27-cp27m-win32.whl", "yanked": false }, { "core-metadata": { "sha256": "eb5621acdfd1a084efcc04d1a5292117422c616dc2ea004d8e760aadbd3f8388" }, "data-dist-info-metadata": { "sha256": "eb5621acdfd1a084efcc04d1a5292117422c616dc2ea004d8e760aadbd3f8388" }, "filename": "PyYAML-3.13-cp27-cp27m-win_amd64.whl", "hashes": { "sha256": "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537" }, "provenance": null, "requires-python": null, "size": 209872, "upload-time": "2018-07-05T22:53:16.904443Z", "url": "https://files.pythonhosted.org/packages/df/4d/1ef8d60464a171112401e17a3a3e88fdb1d5b44af7606e8652b2f39ee9ce/PyYAML-3.13-cp27-cp27m-win_amd64.whl", "yanked": false }, { "core-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "data-dist-info-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "filename": "PyYAML-3.13-cp34-cp34m-win32.whl", "hashes": { "sha256": "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3" }, "provenance": null, "requires-python": null, "size": 192898, "upload-time": "2018-07-05T22:53:19.190872Z", "url": "https://files.pythonhosted.org/packages/35/f0/cf0363b5c431c3a828284903aeacc6bdbba342fd4d7871dda9a3b0b00d15/PyYAML-3.13-cp34-cp34m-win32.whl", "yanked": false }, { "core-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "data-dist-info-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "filename": "PyYAML-3.13-cp34-cp34m-win_amd64.whl", "hashes": { "sha256": "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04" }, "provenance": null, "requires-python": null, "size": 206242, "upload-time": "2018-07-05T22:53:20.770605Z", "url": "https://files.pythonhosted.org/packages/8c/bc/8950092a86259dc511e02a4c3a517ed4b28a254e4da134e3c04e5264e5a3/PyYAML-3.13-cp34-cp34m-win_amd64.whl", "yanked": false }, { "core-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "data-dist-info-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "filename": "PyYAML-3.13-cp35-cp35m-win32.whl", "hashes": { "sha256": "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1" }, "provenance": null, "requires-python": null, "size": 187499, "upload-time": "2018-07-05T22:53:22.576919Z", "url": "https://files.pythonhosted.org/packages/29/33/8bbcd3740d9e96cfb57427b8db7a12093402a3a83f2054887e027b2849de/PyYAML-3.13-cp35-cp35m-win32.whl", "yanked": false }, { "core-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "data-dist-info-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "filename": "PyYAML-3.13-cp35-cp35m-win_amd64.whl", "hashes": { "sha256": "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613" }, "provenance": null, "requires-python": null, "size": 205387, "upload-time": "2018-07-05T22:53:24.438646Z", "url": "https://files.pythonhosted.org/packages/ad/d4/d895fb7ac1b0828151b829a32cefc8a8b58b4499570520b91af20982b880/PyYAML-3.13-cp35-cp35m-win_amd64.whl", "yanked": false }, { "core-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "data-dist-info-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "filename": "PyYAML-3.13-cp36-cp36m-win32.whl", "hashes": { "sha256": "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a" }, "provenance": null, "requires-python": null, "size": 188186, "upload-time": "2018-07-05T22:53:25.923669Z", "url": "https://files.pythonhosted.org/packages/fb/51/0c49c6caafe8d9a27ad9b0ca9f91adda5a5072b9efbbe7585fb97a4c71c4/PyYAML-3.13-cp36-cp36m-win32.whl", "yanked": false }, { "core-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "data-dist-info-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "filename": "PyYAML-3.13-cp36-cp36m-win_amd64.whl", "hashes": { "sha256": "3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b" }, "provenance": null, "requires-python": null, "size": 206277, "upload-time": "2018-07-05T22:53:27.386610Z", "url": "https://files.pythonhosted.org/packages/4f/ca/5fad249c5032270540c24d2189b0ddf1396aac49b0bdc548162edcf14131/PyYAML-3.13-cp36-cp36m-win_amd64.whl", "yanked": false }, { "core-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "data-dist-info-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "filename": "PyYAML-3.13-cp37-cp37m-win32.whl", "hashes": { "sha256": "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" }, "provenance": null, "requires-python": null, "size": 188313, "upload-time": "2018-07-05T22:53:28.995194Z", "url": "https://files.pythonhosted.org/packages/5c/ed/d6557f70daaaab6ee5cd2f8ccf7bedd63081e522e38679c03840e1acc114/PyYAML-3.13-cp37-cp37m-win32.whl", "yanked": false }, { "core-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "data-dist-info-metadata": { "sha256": "bb6a78439e80d7904471d134b006d366645dfdf9241495f8a9626f778f9dc5bd" }, "filename": "PyYAML-3.13-cp37-cp37m-win_amd64.whl", "hashes": { "sha256": "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1" }, "provenance": null, "requires-python": null, "size": 206614, "upload-time": "2018-07-05T22:53:30.864210Z", "url": "https://files.pythonhosted.org/packages/bf/96/d02ef8e1f3073e07ffdc240444e5041f403f29c0775f9f1653f18221082f/PyYAML-3.13-cp37-cp37m-win_amd64.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "PyYAML-3.13.tar.gz", "hashes": { "sha256": "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf" }, "provenance": null, "requires-python": null, "size": 270607, "upload-time": "2018-07-05T22:52:16.800539Z", "url": "https://files.pythonhosted.org/packages/9e/a3/1d13970c3f36777c583f136c136f804d70f500168edc1edea6daa7200769/PyYAML-3.13.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "pyyaml", "project-status": { "status": "active" }, "versions": [ "3.13.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/requests/2.18.0.json ================================================ { "info": { "author": "Kenneth Reitz", "author_email": "me@kennethreitz.org", "bugtrack_url": null, "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.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://python-requests.org", "keywords": "", "license": "Apache 2.0", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "requests", "package_url": "https://pypi.org/project/requests/", "platform": "", "project_url": "https://pypi.org/project/requests/", "project_urls": { "Homepage": "http://python-requests.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/requests/2.18.0/", "requires_dist": [ "certifi (>=2017.4.17)", "chardet (>=3.0.2,<3.1.0)", "idna (>=2.5,<2.6)", "urllib3 (<1.22,>=1.21.1)", "cryptography (>=1.3.4); extra == 'security'", "idna (>=2.0.0); extra == 'security'", "pyOpenSSL (>=0.14); extra == 'security'", "PySocks (!=1.5.7,>=1.5.6); extra == 'socks'", "win-inet-pton; sys_platform == \"win32\" and (python_version == \"2.7\" or python_version == \"2.6\") and extra == 'socks'" ], "requires_python": "", "summary": "Python HTTP for Humans.", "version": "2.18.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "e2f0c81405acbf53d0412b984eb3fc578cdd10e347374e1aec074638a500c186", "md5": "6f34e2439fcb3dd1b6e3304903bb6be8", "sha256": "5e88d64aa56ac0fda54e77fb9762ebc65879e171b746d5479a33c4082519d6c6" }, "downloads": -1, "filename": "requests-2.18.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "6f34e2439fcb3dd1b6e3304903bb6be8", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 563596, "upload_time": "2017-06-14T15:44:35", "upload_time_iso_8601": "2017-06-14T15:44:35.080617Z", "url": "https://files.pythonhosted.org/packages/e2/f0/c81405acbf53d0412b984eb3fc578cdd10e347374e1aec074638a500c186/requests-2.18.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "e097e2f972b6826c9cfe57b6934e3773d2783733bc2d345d810bafd309df3d15", "md5": "b8b333ace1653652ddcce95284577f5c", "sha256": "cd0189f962787284bff715fddaad478eb4d9c15aa167bd64e52ea0f661e7ea5c" }, "downloads": -1, "filename": "requests-2.18.0.tar.gz", "has_sig": false, "md5_digest": "b8b333ace1653652ddcce95284577f5c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 124085, "upload_time": "2017-06-14T15:44:37", "upload_time_iso_8601": "2017-06-14T15:44:37.484470Z", "url": "https://files.pythonhosted.org/packages/e0/97/e2f972b6826c9cfe57b6934e3773d2783733bc2d345d810bafd309df3d15/requests-2.18.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/requests/2.18.1.json ================================================ { "info": { "author": "Kenneth Reitz", "author_email": "me@kennethreitz.org", "bugtrack_url": null, "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.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://python-requests.org", "keywords": "", "license": "Apache 2.0", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "requests", "package_url": "https://pypi.org/project/requests/", "platform": "", "project_url": "https://pypi.org/project/requests/", "project_urls": { "Homepage": "http://python-requests.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/requests/2.18.1/", "requires_dist": [ "certifi (>=2017.4.17)", "chardet (>=3.0.2,<3.1.0)", "idna (>=2.5,<2.6)", "urllib3 (<1.22,>=1.21.1)", "cryptography (>=1.3.4); extra == 'security'", "idna (>=2.0.0); extra == 'security'", "pyOpenSSL (>=0.14); extra == 'security'", "PySocks (!=1.5.7,>=1.5.6); extra == 'socks'", "win-inet-pton; sys_platform == \"win32\" and (python_version == \"2.7\" or python_version == \"2.6\") and extra == 'socks'" ], "requires_python": "", "summary": "Python HTTP for Humans.", "version": "2.18.1", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "5a58671011e3ff4a06e2969322267d78dcfda1bf4d1576551df1cce93cd7239d", "md5": "a7fbdc82134a2610b3d0cdc7e59f0bde", "sha256": "6afd3371c1f4c1970497cdcace5c5ecbbe58267bf05ca1abd93d99d170803ab7" }, "downloads": -1, "filename": "requests-2.18.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "a7fbdc82134a2610b3d0cdc7e59f0bde", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 88107, "upload_time": "2017-06-14T17:51:25", "upload_time_iso_8601": "2017-06-14T17:51:25.096686Z", "url": "https://files.pythonhosted.org/packages/5a/58/671011e3ff4a06e2969322267d78dcfda1bf4d1576551df1cce93cd7239d/requests-2.18.1-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "2cb52b6e8ef8dd18203b6399e9f28c7d54f6de7b7549853fe36d575bd31e29a7", "md5": "40f723ed01dddeaf990d0609d073f021", "sha256": "c6f3bdf4a4323ac7b45d01e04a6f6c20e32a052cd04de81e05103abc049ad9b9" }, "downloads": -1, "filename": "requests-2.18.1.tar.gz", "has_sig": false, "md5_digest": "40f723ed01dddeaf990d0609d073f021", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 124229, "upload_time": "2017-06-14T17:51:28", "upload_time_iso_8601": "2017-06-14T17:51:28.960131Z", "url": "https://files.pythonhosted.org/packages/2c/b5/2b6e8ef8dd18203b6399e9f28c7d54f6de7b7549853fe36d575bd31e29a7/requests-2.18.1.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/requests/2.18.2.json ================================================ { "info": { "author": "Kenneth Reitz", "author_email": "me@kennethreitz.org", "bugtrack_url": null, "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.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://python-requests.org", "keywords": "", "license": "Apache 2.0", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "requests", "package_url": "https://pypi.org/project/requests/", "platform": "", "project_url": "https://pypi.org/project/requests/", "project_urls": { "Homepage": "http://python-requests.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/requests/2.18.2/", "requires_dist": [ "certifi (>=2017.4.17)", "chardet (>=3.0.2,<3.1.0)", "idna (>=2.5,<2.6)", "urllib3 (<1.23,>=1.21.1)", "cryptography (>=1.3.4); extra == 'security'", "idna (>=2.0.0); extra == 'security'", "pyOpenSSL (>=0.14); extra == 'security'", "PySocks (!=1.5.7,>=1.5.6); extra == 'socks'", "win-inet-pton; sys_platform == \"win32\" and (python_version == \"2.7\" or python_version == \"2.6\") and extra == 'socks'" ], "requires_python": "", "summary": "Python HTTP for Humans.", "version": "2.18.2", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "cffa31b222e4b44975de1b5ac3e1a725abdfeb00e0d761567ab426ee28a7fc73", "md5": "08026e24839d8bf36d248abfb2b6b674", "sha256": "414459f05392835d4d653b57b8e58f98aea9c6ff2782e37de0a1ee92891ce900" }, "downloads": -1, "filename": "requests-2.18.2-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "08026e24839d8bf36d248abfb2b6b674", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 88342, "upload_time": "2017-07-25T15:23:15", "upload_time_iso_8601": "2017-07-25T15:23:15.338694Z", "url": "https://files.pythonhosted.org/packages/cf/fa/31b222e4b44975de1b5ac3e1a725abdfeb00e0d761567ab426ee28a7fc73/requests-2.18.2-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "072e81fdfdfac91cf3cb2518fb149ac67caf0e081b485eab68e9aee63396f7e8", "md5": "49bd9924d3be341871bc922cde6f372e", "sha256": "5b26fcc5e72757a867e4d562333f841eddcef93548908a1bb1a9207260618da9" }, "downloads": -1, "filename": "requests-2.18.2.tar.gz", "has_sig": false, "md5_digest": "49bd9924d3be341871bc922cde6f372e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 125381, "upload_time": "2017-07-25T15:23:18", "upload_time_iso_8601": "2017-07-25T15:23:18.103843Z", "url": "https://files.pythonhosted.org/packages/07/2e/81fdfdfac91cf3cb2518fb149ac67caf0e081b485eab68e9aee63396f7e8/requests-2.18.2.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/requests/2.18.3.json ================================================ { "info": { "author": "Kenneth Reitz", "author_email": "me@kennethreitz.org", "bugtrack_url": null, "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.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://python-requests.org", "keywords": "", "license": "Apache 2.0", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "requests", "package_url": "https://pypi.org/project/requests/", "platform": "", "project_url": "https://pypi.org/project/requests/", "project_urls": { "Homepage": "http://python-requests.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/requests/2.18.3/", "requires_dist": [ "certifi (>=2017.4.17)", "chardet (>=3.0.2,<3.1.0)", "idna (>=2.5,<2.6)", "urllib3 (<1.23,>=1.21.1)", "cryptography (>=1.3.4); extra == 'security'", "idna (>=2.0.0); extra == 'security'", "pyOpenSSL (>=0.14); extra == 'security'", "PySocks (!=1.5.7,>=1.5.6); extra == 'socks'", "win-inet-pton; sys_platform == \"win32\" and (python_version == \"2.7\" or python_version == \"2.6\") and extra == 'socks'" ], "requires_python": "", "summary": "Python HTTP for Humans.", "version": "2.18.3", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "ba92c35ed010e8f96781f08dfa6d9a6a19445a175a9304aceedece77cd48b68f", "md5": "d2d34c959a45f7da592a383485ad8b8c", "sha256": "b62be4ec5999c24d10c98d248a136e7db20ca6616a2b65060cd9399417331e8a" }, "downloads": -1, "filename": "requests-2.18.3-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "d2d34c959a45f7da592a383485ad8b8c", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 88626, "upload_time": "2017-08-02T13:23:31", "upload_time_iso_8601": "2017-08-02T13:23:31.998938Z", "url": "https://files.pythonhosted.org/packages/ba/92/c35ed010e8f96781f08dfa6d9a6a19445a175a9304aceedece77cd48b68f/requests-2.18.3-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "c338d95ddb6cc8558930600be088e174a2152261a1e0708a18bf91b5b8c90b22", "md5": "c8f60cf816a35c0c3fef0a40d0e407a6", "sha256": "fb68a7baef4965c12d9cd67c0f5a46e6e28be3d8c7b6910c758fbcc99880b518" }, "downloads": -1, "filename": "requests-2.18.3.tar.gz", "has_sig": false, "md5_digest": "c8f60cf816a35c0c3fef0a40d0e407a6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 126008, "upload_time": "2017-08-02T13:23:35", "upload_time_iso_8601": "2017-08-02T13:23:35.599515Z", "url": "https://files.pythonhosted.org/packages/c3/38/d95ddb6cc8558930600be088e174a2152261a1e0708a18bf91b5b8c90b22/requests-2.18.3.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/requests/2.18.4.json ================================================ { "info": { "author": "Kenneth Reitz", "author_email": "me@kennethreitz.org", "bugtrack_url": null, "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.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://python-requests.org", "keywords": "", "license": "Apache 2.0", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "requests", "package_url": "https://pypi.org/project/requests/", "platform": "", "project_url": "https://pypi.org/project/requests/", "project_urls": { "Homepage": "http://python-requests.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/requests/2.18.4/", "requires_dist": [ "certifi (>=2017.4.17)", "chardet (>=3.0.2,<3.1.0)", "idna (>=2.5,<2.7)", "urllib3 (<1.23,>=1.21.1)", "cryptography (>=1.3.4); extra == 'security'", "idna (>=2.0.0); extra == 'security'", "pyOpenSSL (>=0.14); extra == 'security'", "PySocks (!=1.5.7,>=1.5.6); extra == 'socks'", "win-inet-pton; sys_platform == \"win32\" and (python_version == \"2.7\" or python_version == \"2.6\") and extra == 'socks'" ], "requires_python": "", "summary": "Python HTTP for Humans.", "version": "2.18.4", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "e770e65750c42f40b97b0ed738d0f859", "sha256": "098be851f30be5bcb2c7537798d44314f576e53818ba9def25141ae4dce8b25d" }, "downloads": -1, "filename": "requests-2.18.4-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "e770e65750c42f40b97b0ed738d0f859", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 88704, "upload_time": "2017-08-15T13:23:43", "upload_time_iso_8601": "2017-08-15T13:23:43.489631Z", "url": "https://files.pythonhosted.org/packages/49/df/50aa1999ab9bde74656c2919d9c0c085fd2b3775fd3eca826012bef76d8c/requests-2.18.4-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "942a6a383dc94da90cf58f5adcf028a4", "sha256": "ec62f7e0e9d4814656b0172dbd592fea06127c6556ff5651eb5d2c8768671fd4" }, "downloads": -1, "filename": "requests-2.18.4.tar.gz", "has_sig": false, "md5_digest": "942a6a383dc94da90cf58f5adcf028a4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 126224, "upload_time": "2017-08-15T13:23:46", "upload_time_iso_8601": "2017-08-15T13:23:46.348325Z", "url": "https://files.pythonhosted.org/packages/b0/e1/eab4fc3752e3d240468a8c0b284607899d2fbfb236a56b7377a329aa8d09/requests-2.18.4.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/requests/2.19.0.json ================================================ { "info": { "author": "Kenneth Reitz", "author_email": "me@kennethreitz.org", "bugtrack_url": null, "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", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://python-requests.org", "keywords": "", "license": "Apache 2.0", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "requests", "package_url": "https://pypi.org/project/requests/", "platform": "", "project_url": "https://pypi.org/project/requests/", "project_urls": { "Homepage": "http://python-requests.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/requests/2.19.0/", "requires_dist": [ "chardet (<3.1.0,>=3.0.2)", "idna (<2.8,>=2.5)", "urllib3 (<1.24,>=1.21.1)", "certifi (>=2017.4.17)", "pyOpenSSL (>=0.14); extra == 'security'", "cryptography (>=1.3.4); extra == 'security'", "idna (>=2.0.0); extra == 'security'", "PySocks (!=1.5.7,>=1.5.6); extra == 'socks'", "win-inet-pton; sys_platform == \"win32\" and (python_version == \"2.7\" or python_version == \"2.6\") and extra == 'socks'" ], "requires_python": ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "Python HTTP for Humans.", "version": "2.19.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "cc15e1c318dbc20032ffbe5628837ca0de2d5b116ffd1b849c699634010f6a5d", "md5": "1ae2b89f8ea3e4aea8b199987fb2aae9", "sha256": "421cfc8d9dde7d6aff68196420afd86b88c65d77d8da9cf83f4ecad785d7b9d6" }, "downloads": -1, "filename": "requests-2.19.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "1ae2b89f8ea3e4aea8b199987fb2aae9", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 91865, "upload_time": "2018-06-12T14:46:15", "upload_time_iso_8601": "2018-06-12T14:46:15.289074Z", "url": "https://files.pythonhosted.org/packages/cc/15/e1c318dbc20032ffbe5628837ca0de2d5b116ffd1b849c699634010f6a5d/requests-2.19.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "752782da3fa4ea7a8c3526c48eaafe427352ff9c931633b917c2251826a43697", "md5": "8a7844c58d496e9e92481de459830229", "sha256": "cc408268d0e21589bcc2b2c248e42932b8c4d112f499c12c92e99e2178a6134c" }, "downloads": -1, "filename": "requests-2.19.0.tar.gz", "has_sig": false, "md5_digest": "8a7844c58d496e9e92481de459830229", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 130875, "upload_time": "2018-06-12T14:46:17", "upload_time_iso_8601": "2018-06-12T14:46:17.223245Z", "url": "https://files.pythonhosted.org/packages/75/27/82da3fa4ea7a8c3526c48eaafe427352ff9c931633b917c2251826a43697/requests-2.19.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/requests.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "4beb28df7dccdb2c54530560738dda249189e114357a69d5e5e7f06a2a019197" }, "data-dist-info-metadata": { "sha256": "4beb28df7dccdb2c54530560738dda249189e114357a69d5e5e7f06a2a019197" }, "filename": "requests-2.18.0-py2.py3-none-any.whl", "hashes": { "sha256": "5e88d64aa56ac0fda54e77fb9762ebc65879e171b746d5479a33c4082519d6c6" }, "provenance": null, "requires-python": null, "size": 563596, "upload-time": "2017-06-14T15:44:35.080617Z", "url": "https://files.pythonhosted.org/packages/e2/f0/c81405acbf53d0412b984eb3fc578cdd10e347374e1aec074638a500c186/requests-2.18.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "requests-2.18.0.tar.gz", "hashes": { "sha256": "cd0189f962787284bff715fddaad478eb4d9c15aa167bd64e52ea0f661e7ea5c" }, "provenance": null, "requires-python": null, "size": 124085, "upload-time": "2017-06-14T15:44:37.484470Z", "url": "https://files.pythonhosted.org/packages/e0/97/e2f972b6826c9cfe57b6934e3773d2783733bc2d345d810bafd309df3d15/requests-2.18.0.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "4e4599f783a6950eb72a9d78fecb2f3438f07237e83708a587beda8a335b5f94" }, "data-dist-info-metadata": { "sha256": "4e4599f783a6950eb72a9d78fecb2f3438f07237e83708a587beda8a335b5f94" }, "filename": "requests-2.18.1-py2.py3-none-any.whl", "hashes": { "sha256": "6afd3371c1f4c1970497cdcace5c5ecbbe58267bf05ca1abd93d99d170803ab7" }, "provenance": null, "requires-python": null, "size": 88107, "upload-time": "2017-06-14T17:51:25.096686Z", "url": "https://files.pythonhosted.org/packages/5a/58/671011e3ff4a06e2969322267d78dcfda1bf4d1576551df1cce93cd7239d/requests-2.18.1-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "requests-2.18.1.tar.gz", "hashes": { "sha256": "c6f3bdf4a4323ac7b45d01e04a6f6c20e32a052cd04de81e05103abc049ad9b9" }, "provenance": null, "requires-python": null, "size": 124229, "upload-time": "2017-06-14T17:51:28.960131Z", "url": "https://files.pythonhosted.org/packages/2c/b5/2b6e8ef8dd18203b6399e9f28c7d54f6de7b7549853fe36d575bd31e29a7/requests-2.18.1.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "2f28a31e586b7d1d382e1b64b48ce26d5ff18781f9eca0fba7654b55d7a76549" }, "data-dist-info-metadata": { "sha256": "2f28a31e586b7d1d382e1b64b48ce26d5ff18781f9eca0fba7654b55d7a76549" }, "filename": "requests-2.18.2-py2.py3-none-any.whl", "hashes": { "sha256": "414459f05392835d4d653b57b8e58f98aea9c6ff2782e37de0a1ee92891ce900" }, "provenance": null, "requires-python": null, "size": 88342, "upload-time": "2017-07-25T15:23:15.338694Z", "url": "https://files.pythonhosted.org/packages/cf/fa/31b222e4b44975de1b5ac3e1a725abdfeb00e0d761567ab426ee28a7fc73/requests-2.18.2-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "requests-2.18.2.tar.gz", "hashes": { "sha256": "5b26fcc5e72757a867e4d562333f841eddcef93548908a1bb1a9207260618da9" }, "provenance": null, "requires-python": null, "size": 125381, "upload-time": "2017-07-25T15:23:18.103843Z", "url": "https://files.pythonhosted.org/packages/07/2e/81fdfdfac91cf3cb2518fb149ac67caf0e081b485eab68e9aee63396f7e8/requests-2.18.2.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "b486edffe7b597477bf5645377f50ad2cd4db97413c3e74df3051d9b0db41ac4" }, "data-dist-info-metadata": { "sha256": "b486edffe7b597477bf5645377f50ad2cd4db97413c3e74df3051d9b0db41ac4" }, "filename": "requests-2.18.3-py2.py3-none-any.whl", "hashes": { "sha256": "b62be4ec5999c24d10c98d248a136e7db20ca6616a2b65060cd9399417331e8a" }, "provenance": null, "requires-python": null, "size": 88626, "upload-time": "2017-08-02T13:23:31.998938Z", "url": "https://files.pythonhosted.org/packages/ba/92/c35ed010e8f96781f08dfa6d9a6a19445a175a9304aceedece77cd48b68f/requests-2.18.3-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "requests-2.18.3.tar.gz", "hashes": { "sha256": "fb68a7baef4965c12d9cd67c0f5a46e6e28be3d8c7b6910c758fbcc99880b518" }, "provenance": null, "requires-python": null, "size": 126008, "upload-time": "2017-08-02T13:23:35.599515Z", "url": "https://files.pythonhosted.org/packages/c3/38/d95ddb6cc8558930600be088e174a2152261a1e0708a18bf91b5b8c90b22/requests-2.18.3.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "37a7c01ccfdf63746e53a70d20723cb095be3f8048582ce38673adc640d14dfd" }, "data-dist-info-metadata": { "sha256": "37a7c01ccfdf63746e53a70d20723cb095be3f8048582ce38673adc640d14dfd" }, "filename": "requests-2.18.4-py2.py3-none-any.whl", "hashes": { "md5": "e770e65750c42f40b97b0ed738d0f859", "sha256": "098be851f30be5bcb2c7537798d44314f576e53818ba9def25141ae4dce8b25d" }, "provenance": null, "requires-python": null, "size": 88704, "upload-time": "2017-08-15T13:23:43.489631Z", "url": "https://files.pythonhosted.org/packages/49/df/50aa1999ab9bde74656c2919d9c0c085fd2b3775fd3eca826012bef76d8c/requests-2.18.4-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "requests-2.18.4.tar.gz", "hashes": { "md5": "942a6a383dc94da90cf58f5adcf028a4", "sha256": "ec62f7e0e9d4814656b0172dbd592fea06127c6556ff5651eb5d2c8768671fd4" }, "provenance": null, "requires-python": null, "size": 126224, "upload-time": "2017-08-15T13:23:46.348325Z", "url": "https://files.pythonhosted.org/packages/b0/e1/eab4fc3752e3d240468a8c0b284607899d2fbfb236a56b7377a329aa8d09/requests-2.18.4.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "e022ab2bfbbfbf3b77f29b8bc3115eb5b202c651b9b928ec9c0b7ef9a16070f3" }, "data-dist-info-metadata": { "sha256": "e022ab2bfbbfbf3b77f29b8bc3115eb5b202c651b9b928ec9c0b7ef9a16070f3" }, "filename": "requests-2.19.0-py2.py3-none-any.whl", "hashes": { "sha256": "421cfc8d9dde7d6aff68196420afd86b88c65d77d8da9cf83f4ecad785d7b9d6" }, "provenance": null, "requires-python": ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 91865, "upload-time": "2018-06-12T14:46:15.289074Z", "url": "https://files.pythonhosted.org/packages/cc/15/e1c318dbc20032ffbe5628837ca0de2d5b116ffd1b849c699634010f6a5d/requests-2.19.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "requests-2.19.0.tar.gz", "hashes": { "sha256": "cc408268d0e21589bcc2b2c248e42932b8c4d112f499c12c92e99e2178a6134c" }, "provenance": null, "requires-python": ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 130875, "upload-time": "2018-06-12T14:46:17.223245Z", "url": "https://files.pythonhosted.org/packages/75/27/82da3fa4ea7a8c3526c48eaafe427352ff9c931633b917c2251826a43697/requests-2.19.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "requests", "project-status": { "status": "active" }, "versions": [ "2.18.0", "2.18.1", "2.18.2", "2.18.3", "2.18.4", "2.19.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/setuptools/39.2.0.json ================================================ { "info": { "author": "Python Packaging Authority", "author_email": "distutils-sig@python.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Archiving :: Packaging", "Topic :: System :: Systems Administration", "Topic :: Utilities" ], "description": "", "description_content_type": "text/x-rst; charset=UTF-8", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/pypa/setuptools", "keywords": "CPAN PyPI distutils eggs package management", "license": "", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "setuptools", "package_url": "https://pypi.org/project/setuptools/", "platform": "", "project_url": "https://pypi.org/project/setuptools/", "project_urls": { "Documentation": "https://setuptools.readthedocs.io/", "Homepage": "https://github.com/pypa/setuptools" }, "provides_extra": null, "release_url": "https://pypi.org/project/setuptools/39.2.0/", "requires_dist": [ "certifi (==2016.9.26); extra == 'certs'", "wincertstore (==0.2); (sys_platform=='win32') and extra == 'ssl'" ], "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*", "summary": "Easily download, build, install, upgrade, and uninstall Python packages", "version": "39.2.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "7fe1820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92", "md5": "8d066d2201311ed30be535b473e32fed", "sha256": "8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926" }, "downloads": -1, "filename": "setuptools-39.2.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "8d066d2201311ed30be535b473e32fed", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*", "size": 567556, "upload_time": "2018-05-19T19:19:22", "upload_time_iso_8601": "2018-05-19T19:19:22.625819Z", "url": "https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "1a04d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00", "md5": "dd4e3fa83a21bf7bf9c51026dc8a4e59", "sha256": "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2" }, "downloads": -1, "filename": "setuptools-39.2.0.zip", "has_sig": false, "md5_digest": "dd4e3fa83a21bf7bf9c51026dc8a4e59", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*", "size": 851112, "upload_time": "2018-05-19T19:19:24", "upload_time_iso_8601": "2018-05-19T19:19:24.480740Z", "url": "https://files.pythonhosted.org/packages/1a/04/d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00/setuptools-39.2.0.zip", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/setuptools/67.6.1.json ================================================ { "info": { "author": "Python Packaging Authority", "author_email": "distutils-sig@python.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Archiving :: Packaging", "Topic :: System :: Systems Administration", "Topic :: Utilities" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/pypa/setuptools", "keywords": "CPAN PyPI distutils eggs package management", "license": "", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "setuptools", "package_url": "https://pypi.org/project/setuptools/", "platform": null, "project_url": "https://pypi.org/project/setuptools/", "project_urls": { "Changelog": "https://setuptools.pypa.io/en/stable/history.html", "Documentation": "https://setuptools.pypa.io/", "Homepage": "https://github.com/pypa/setuptools" }, "provides_extra": null, "release_url": "https://pypi.org/project/setuptools/67.6.1/", "requires_dist": [ "sphinx (>=3.5) ; extra == 'docs'", "jaraco.packaging (>=9) ; extra == 'docs'", "rst.linker (>=1.9) ; extra == 'docs'", "furo ; extra == 'docs'", "sphinx-lint ; extra == 'docs'", "jaraco.tidelift (>=1.4) ; extra == 'docs'", "pygments-github-lexers (==0.0.5) ; extra == 'docs'", "sphinx-favicon ; extra == 'docs'", "sphinx-inline-tabs ; extra == 'docs'", "sphinx-reredirects ; extra == 'docs'", "sphinxcontrib-towncrier ; extra == 'docs'", "sphinx-notfound-page (==0.8.3) ; extra == 'docs'", "sphinx-hoverxref (<2) ; extra == 'docs'", "pytest (>=6) ; extra == 'testing'", "pytest-checkdocs (>=2.4) ; extra == 'testing'", "flake8 (<5) ; extra == 'testing'", "pytest-enabler (>=1.3) ; extra == 'testing'", "pytest-perf ; extra == 'testing'", "flake8-2020 ; extra == 'testing'", "virtualenv (>=13.0.0) ; extra == 'testing'", "wheel ; extra == 'testing'", "pip (>=19.1) ; extra == 'testing'", "jaraco.envs (>=2.2) ; extra == 'testing'", "pytest-xdist ; extra == 'testing'", "jaraco.path (>=3.2.0) ; extra == 'testing'", "build[virtualenv] ; extra == 'testing'", "filelock (>=3.4.0) ; extra == 'testing'", "pip-run (>=8.8) ; extra == 'testing'", "ini2toml[lite] (>=0.9) ; extra == 'testing'", "tomli-w (>=1.0.0) ; extra == 'testing'", "pytest-timeout ; extra == 'testing'", "pytest ; extra == 'testing-integration'", "pytest-xdist ; extra == 'testing-integration'", "pytest-enabler ; extra == 'testing-integration'", "virtualenv (>=13.0.0) ; extra == 'testing-integration'", "tomli ; extra == 'testing-integration'", "wheel ; extra == 'testing-integration'", "jaraco.path (>=3.2.0) ; extra == 'testing-integration'", "jaraco.envs (>=2.2) ; extra == 'testing-integration'", "build[virtualenv] ; extra == 'testing-integration'", "filelock (>=3.4.0) ; extra == 'testing-integration'", "pytest-black (>=0.3.7) ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", "pytest-cov ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", "pytest-mypy (>=0.9.1) ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", "pytest-flake8 ; (python_version < \"3.12\") and extra == 'testing'" ], "requires_python": ">=3.7", "summary": "Easily download, build, install, upgrade, and uninstall Python packages", "version": "67.6.1", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "3b5b846e000da033d54eeaaf7915126e", "sha256": "e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078" }, "downloads": -1, "filename": "setuptools-67.6.1-py3-none-any.whl", "has_sig": false, "md5_digest": "3b5b846e000da033d54eeaaf7915126e", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 1089263, "upload_time": "2023-03-28T13:45:43", "upload_time_iso_8601": "2023-03-28T13:45:43.525946Z", "url": "https://files.pythonhosted.org/packages/0b/fc/8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7/setuptools-67.6.1-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "ee2562f783544d1f95022c906dd3cf98", "sha256": "a737d365c957dd3fced9ddd246118e95dce7a62c3dc49f37e7fdd9e93475d785" }, "downloads": -1, "filename": "setuptools-67.6.1.tar.gz", "has_sig": false, "md5_digest": "ee2562f783544d1f95022c906dd3cf98", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 2486256, "upload_time": "2023-03-28T13:45:45", "upload_time_iso_8601": "2023-03-28T13:45:45.967259Z", "url": "https://files.pythonhosted.org/packages/cb/46/22ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146/setuptools-67.6.1.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/setuptools.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "73f25249f8930d4b8f842a9e71ed57cf898d19b28beac684454b9c8a1c0cc2d8" }, "data-dist-info-metadata": { "sha256": "73f25249f8930d4b8f842a9e71ed57cf898d19b28beac684454b9c8a1c0cc2d8" }, "filename": "setuptools-39.2.0-py2.py3-none-any.whl", "hashes": { "sha256": "8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926" }, "provenance": null, "requires-python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*", "size": 567556, "upload-time": "2018-05-19T19:19:22.625819Z", "url": "https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "setuptools-39.2.0.zip", "hashes": { "sha256": "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2" }, "provenance": null, "requires-python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*", "size": 851112, "upload-time": "2018-05-19T19:19:24.480740Z", "url": "https://files.pythonhosted.org/packages/1a/04/d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00/setuptools-39.2.0.zip", "yanked": false }, { "core-metadata": { "sha256": "4cf21fee4bb46056adb125243cf6d0f5e1efafab06fae67223b43631dbd81cb6" }, "data-dist-info-metadata": { "sha256": "4cf21fee4bb46056adb125243cf6d0f5e1efafab06fae67223b43631dbd81cb6" }, "filename": "setuptools-67.6.1-py3-none-any.whl", "hashes": { "md5": "3b5b846e000da033d54eeaaf7915126e", "sha256": "e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078" }, "provenance": null, "requires-python": ">=3.7", "size": 1089263, "upload-time": "2023-03-28T13:45:43.525946Z", "url": "https://files.pythonhosted.org/packages/0b/fc/8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7/setuptools-67.6.1-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "setuptools-67.6.1.tar.gz", "hashes": { "md5": "ee2562f783544d1f95022c906dd3cf98", "sha256": "a737d365c957dd3fced9ddd246118e95dce7a62c3dc49f37e7fdd9e93475d785" }, "provenance": null, "requires-python": ">=3.7", "size": 2486256, "upload-time": "2023-03-28T13:45:45.967259Z", "url": "https://files.pythonhosted.org/packages/cb/46/22ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146/setuptools-67.6.1.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "setuptools", "project-status": { "status": "active" }, "versions": [ "39.2.0", "67.6.1" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/six/1.11.0.json ================================================ { "info": { "author": "Benjamin Peterson", "author_email": "benjamin@python.org", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries", "Topic :: Utilities" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pypi.python.org/pypi/six/", "keywords": "", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "six", "package_url": "https://pypi.org/project/six/", "platform": "", "project_url": "https://pypi.org/project/six/", "project_urls": { "Homepage": "http://pypi.python.org/pypi/six/" }, "provides_extra": null, "release_url": "https://pypi.org/project/six/1.11.0/", "requires_dist": null, "requires_python": "", "summary": "Python 2 and 3 compatibility utilities", "version": "1.11.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "9500094701f7201ddd065c60abcefef1", "sha256": "534e9875e44a507adec601c29b3cbd2ca6dae7df92bf3dd20c7289b2f99f7466" }, "downloads": -1, "filename": "six-1.11.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "9500094701f7201ddd065c60abcefef1", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 10702, "upload_time": "2017-09-17T18:46:53", "upload_time_iso_8601": "2017-09-17T18:46:53.702194Z", "url": "https://files.pythonhosted.org/packages/67/4b/141a581104b1f6397bfa78ac9d43d8ad29a7ca43ea90a2d863fe3056e86a/six-1.11.0-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "25d3568604f921dd23532b88a0ce17e7", "sha256": "268a4ccb159c1a2d2c79336b02e75058387b0cdbb4cea2f07846a758f48a356d" }, "downloads": -1, "filename": "six-1.11.0.tar.gz", "has_sig": false, "md5_digest": "25d3568604f921dd23532b88a0ce17e7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29860, "upload_time": "2017-09-17T18:46:54", "upload_time_iso_8601": "2017-09-17T18:46:54.492027Z", "url": "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six-1.11.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/six.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "d8e61e1b2a777206b74022118a9dd3410b5bee46bd65366c521ae4c5246c2019" }, "data-dist-info-metadata": { "sha256": "d8e61e1b2a777206b74022118a9dd3410b5bee46bd65366c521ae4c5246c2019" }, "filename": "six-1.11.0-py2.py3-none-any.whl", "hashes": { "md5": "9500094701f7201ddd065c60abcefef1", "sha256": "534e9875e44a507adec601c29b3cbd2ca6dae7df92bf3dd20c7289b2f99f7466" }, "provenance": null, "requires-python": null, "size": 10702, "upload-time": "2017-09-17T18:46:53.702194Z", "url": "https://files.pythonhosted.org/packages/67/4b/141a581104b1f6397bfa78ac9d43d8ad29a7ca43ea90a2d863fe3056e86a/six-1.11.0-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "six-1.11.0.tar.gz", "hashes": { "md5": "25d3568604f921dd23532b88a0ce17e7", "sha256": "268a4ccb159c1a2d2c79336b02e75058387b0cdbb4cea2f07846a758f48a356d" }, "provenance": null, "requires-python": null, "size": 29860, "upload-time": "2017-09-17T18:46:54.492027Z", "url": "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six-1.11.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "six", "project-status": { "status": "active" }, "versions": [ "1.11.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/sqlalchemy/1.2.12.json ================================================ { "info": { "author": "Mike Bayer", "author_email": "mike_mp@zzzcomputing.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://www.sqlalchemy.org", "keywords": "", "license": "MIT License", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "SQLAlchemy", "package_url": "https://pypi.org/project/SQLAlchemy/", "platform": "", "project_url": "https://pypi.org/project/SQLAlchemy/", "project_urls": { "Homepage": "http://www.sqlalchemy.org" }, "provides_extra": null, "release_url": "https://pypi.org/project/SQLAlchemy/1.2.12/", "requires_dist": null, "requires_python": "", "summary": "Database Abstraction Library", "version": "1.2.12", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "4a2617b5254748828d09349fc4eff6bd", "sha256": "b5a127599b3f27847fba6119de0fcb70832a8041b103701a708b7c7d044faa38" }, "downloads": -1, "filename": "SQLAlchemy-1.2.12.tar.gz", "has_sig": false, "md5_digest": "4a2617b5254748828d09349fc4eff6bd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 5634807, "upload_time": "2018-09-19T18:14:55", "upload_time_iso_8601": "2018-09-19T18:14:55.299706Z", "url": "https://files.pythonhosted.org/packages/25/c9/b0552098cee325425a61efdf380c51b5c721e459081c85bbb860f501c091/SQLAlchemy-1.2.12.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/sqlalchemy.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "SQLAlchemy-1.2.12.tar.gz", "hashes": { "md5": "4a2617b5254748828d09349fc4eff6bd", "sha256": "b5a127599b3f27847fba6119de0fcb70832a8041b103701a708b7c7d044faa38" }, "provenance": null, "requires-python": null, "size": 5634807, "upload-time": "2018-09-19T18:14:55.299706Z", "url": "https://files.pythonhosted.org/packages/25/c9/b0552098cee325425a61efdf380c51b5c721e459081c85bbb860f501c091/SQLAlchemy-1.2.12.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "sqlalchemy", "project-status": { "status": "active" }, "versions": [ "1.2.12" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/toga/0.3.0.json ================================================ { "info": { "author": "Russell Keith-Magee", "author_email": "russell@keith-magee.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets" ], "description": "", "description_content_type": "text/x-rst; charset=UTF-8", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://beeware.org/project/projects/libraries/toga/", "keywords": "gui,widget,cross-platform,desktop,mobile,web,macOS,cocoa,iOS,android,windows,winforms,linux,gtk", "license": "New BSD", "license_expression": null, "license_files": null, "maintainer": "BeeWare Team", "maintainer_email": "team@beeware.org", "name": "toga", "package_url": "https://pypi.org/project/toga/", "platform": null, "project_url": "https://pypi.org/project/toga/", "project_urls": { "Documentation": "http://toga.readthedocs.io/en/latest/", "Funding": "https://beeware.org/contributing/membership/", "Homepage": "https://beeware.org/project/projects/libraries/toga/", "Source": "https://github.com/beeware/toga", "Tracker": "https://github.com/beeware/toga/issues" }, "provides_extra": null, "release_url": "https://pypi.org/project/toga/0.3.0/", "requires_dist": [ "toga-cocoa (==0.3.0) ; sys_platform==\"darwin\"", "toga-gtk (==0.3.0) ; sys_platform==\"linux\"", "toga-winforms (==0.3.0) ; sys_platform==\"win32\"" ], "requires_python": ">=3.7", "summary": "A Python native, OS native GUI toolkit.", "version": "0.3.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "401df3f98e3811ef657aa2acabe039fcbbcae7fc3463f3e18a2458e099465a49", "md5": "4c9f80d34e8d3f6a3cb301749fbca4ca", "sha256": "846f1598d5ce91e44aefaac8abc9ba8c5a7c3bbd8c69d7004b92ef64228500ad" }, "downloads": -1, "filename": "toga-0.3.0-py3-none-any.whl", "has_sig": false, "md5_digest": "4c9f80d34e8d3f6a3cb301749fbca4ca", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 1945, "upload_time": "2023-01-30T03:29:31", "upload_time_iso_8601": "2023-01-30T03:29:31.034500Z", "url": "https://files.pythonhosted.org/packages/40/1d/f3f98e3811ef657aa2acabe039fcbbcae7fc3463f3e18a2458e099465a49/toga-0.3.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "f3cf44b2d505e13b2ec0c7cef2677ac5d8bffb1fb22c4c1d3671179f8a52a40b", "md5": "f6d6ddeffa5c08a875d436b2dc835e1d", "sha256": "0d1f04d6b5773b8682e5eb1260ec9f07b43f950b89916f9406b2db33cde27240" }, "downloads": -1, "filename": "toga-0.3.0.tar.gz", "has_sig": false, "md5_digest": "f6d6ddeffa5c08a875d436b2dc835e1d", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 3236, "upload_time": "2023-01-30T03:29:34", "upload_time_iso_8601": "2023-01-30T03:29:34.595566Z", "url": "https://files.pythonhosted.org/packages/f3/cf/44b2d505e13b2ec0c7cef2677ac5d8bffb1fb22c4c1d3671179f8a52a40b/toga-0.3.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/toga/0.3.0dev1.json ================================================ { "info": { "author": "Russell Keith-Magee", "author_email": "russell@keith-magee.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Software Development", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pybee.org/toga", "keywords": "", "license": "New BSD", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "toga", "package_url": "https://pypi.org/project/toga/", "platform": "", "project_url": "https://pypi.org/project/toga/", "project_urls": { "Homepage": "http://pybee.org/toga" }, "provides_extra": null, "release_url": "https://pypi.org/project/toga/0.3.0.dev1/", "requires_dist": [ "toga-cocoa; sys_platform==\"darwin\"", "toga-gtk; sys_platform==\"linux\"", "toga-winforms; sys_platform==\"win32\"" ], "requires_python": "", "summary": "A Python native, OS native GUI toolkit.", "version": "0.3.0.dev1", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "ebefea806c706d3dc90d4bc8c412c0ad3515fd018074f5fdd4bd020bdd4c0c80", "md5": "7b219b2249b825f28051aae0230f2818", "sha256": "8553bf332d8fbf39b500745ed9c4044a846fbba68e31de70e6fe83fdffcb0a9e" }, "downloads": -1, "filename": "toga-0.3.0.dev1-py3-none-any.whl", "has_sig": false, "md5_digest": "7b219b2249b825f28051aae0230f2818", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 4789, "upload_time": "2018-01-14T04:10:57", "upload_time_iso_8601": "2018-01-14T04:10:57.825822Z", "url": "https://files.pythonhosted.org/packages/eb/ef/ea806c706d3dc90d4bc8c412c0ad3515fd018074f5fdd4bd020bdd4c0c80/toga-0.3.0.dev1-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "4f71c55c15950f7275e761fe53fb0dc83fe4f5fba6199d7b8fb05d741dd33566", "md5": "21b8fff4d110ddfb8c3eb939d7b35a1e", "sha256": "4e5c77056792168a4e84c84bb7214dfb614b79f289dcbe1525be614483496439" }, "downloads": -1, "filename": "toga-0.3.0.dev1.tar.gz", "has_sig": false, "md5_digest": "21b8fff4d110ddfb8c3eb939d7b35a1e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 40187, "upload_time": "2018-01-14T04:11:03", "upload_time_iso_8601": "2018-01-14T04:11:03.212494Z", "url": "https://files.pythonhosted.org/packages/4f/71/c55c15950f7275e761fe53fb0dc83fe4f5fba6199d7b8fb05d741dd33566/toga-0.3.0.dev1.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/toga/0.3.0dev2.json ================================================ { "info": { "author": "Russell Keith-Magee", "author_email": "russell@keith-magee.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Software Development", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets" ], "description": "", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://pybee.org/toga", "keywords": "", "license": "New BSD", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "toga", "package_url": "https://pypi.org/project/toga/", "platform": "", "project_url": "https://pypi.org/project/toga/", "project_urls": { "Homepage": "http://pybee.org/toga" }, "provides_extra": null, "release_url": "https://pypi.org/project/toga/0.3.0.dev2/", "requires_dist": [ "toga-cocoa; sys_platform==\"darwin\"", "toga-gtk; sys_platform==\"linux\"", "toga-winforms; sys_platform==\"win32\"" ], "requires_python": "", "summary": "A Python native, OS native GUI toolkit.", "version": "0.3.0.dev2", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "3a68d1f6feb2ded26b9f6c36cd2a826e895e0fa6bba5fe489ec30b9f3bc1dbea", "md5": "1aa1d5f48b81475569ea80ea04db8852", "sha256": "6e0a2f800a351bbe8639802954d8d283a52b8cdde378541610ff2bfb3b24ad2f" }, "downloads": -1, "filename": "toga-0.3.0.dev2-py3-none-any.whl", "has_sig": false, "md5_digest": "1aa1d5f48b81475569ea80ea04db8852", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 4788, "upload_time": "2018-01-14T04:52:03", "upload_time_iso_8601": "2018-01-14T04:52:03.658804Z", "url": "https://files.pythonhosted.org/packages/3a/68/d1f6feb2ded26b9f6c36cd2a826e895e0fa6bba5fe489ec30b9f3bc1dbea/toga-0.3.0.dev2-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "e36a3264b3d48733cac7546fee02fbc516621574252f7d86546255532b095415", "md5": "a6558d0c5ba3cd763084e564001e9d24", "sha256": "630d2f932bf7aba3a143d3a332190a46a0a3895f509099a20f033caadf131b76" }, "downloads": -1, "filename": "toga-0.3.0.dev2.tar.gz", "has_sig": false, "md5_digest": "a6558d0c5ba3cd763084e564001e9d24", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 40203, "upload_time": "2018-01-14T04:52:09", "upload_time_iso_8601": "2018-01-14T04:52:09.347038Z", "url": "https://files.pythonhosted.org/packages/e3/6a/3264b3d48733cac7546fee02fbc516621574252f7d86546255532b095415/toga-0.3.0.dev2.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/toga/0.4.0.json ================================================ { "info": { "author": "Russell Keith-Magee", "author_email": "russell@keith-magee.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets" ], "description": "", "description_content_type": "text/x-rst; charset=UTF-8", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://beeware.org/project/projects/libraries/toga/", "keywords": "gui,widget,cross-platform,desktop,mobile,web,macOS,cocoa,iOS,android,windows,winforms,linux,freeBSD,gtk", "license": "New BSD", "license_expression": null, "license_files": null, "maintainer": "BeeWare Team", "maintainer_email": "team@beeware.org", "name": "toga", "package_url": "https://pypi.org/project/toga/", "platform": null, "project_url": "https://pypi.org/project/toga/", "project_urls": { "Documentation": "http://toga.readthedocs.io/en/latest/", "Funding": "https://beeware.org/contributing/membership/", "Homepage": "https://beeware.org/project/projects/libraries/toga/", "Source": "https://github.com/beeware/toga", "Tracker": "https://github.com/beeware/toga/issues" }, "provides_extra": null, "release_url": "https://pypi.org/project/toga/0.4.0/", "requires_dist": [ "toga-gtk ==0.4.0 ; \"freebsd\" in sys_platform", "toga-cocoa ==0.4.0 ; sys_platform==\"darwin\"", "toga-gtk ==0.4.0 ; sys_platform==\"linux\"", "toga-winforms ==0.4.0 ; sys_platform==\"win32\"" ], "requires_python": ">=3.8", "summary": "A Python native, OS native GUI toolkit.", "version": "0.4.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "blake2b_256": "365342de9ed8de0d256b785076ac7cf82eaa088f50df8ed2a8ab1011313e8f04", "md5": "8a4f02f7c8a0353f3205801eadf3e3c9", "sha256": "cd2e47bb19ad7dfe0447f1379d4a7e8d9849a25ab8db62d41358185dcb4e4636" }, "downloads": -1, "filename": "toga-0.4.0-py3-none-any.whl", "has_sig": false, "md5_digest": "8a4f02f7c8a0353f3205801eadf3e3c9", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.8", "size": 2270, "upload_time": "2023-11-03T04:09:37", "upload_time_iso_8601": "2023-11-03T04:09:37.311582Z", "url": "https://files.pythonhosted.org/packages/36/53/42de9ed8de0d256b785076ac7cf82eaa088f50df8ed2a8ab1011313e8f04/toga-0.4.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "blake2b_256": "55607dc01b43f991228fd73679e40dc000e4c28a76b8b27b4d155e5bf716ff06", "md5": "bd31edb40d9176b268250a6808b24d56", "sha256": "5ced4a0c85399a52e7c2397e7326f787a04da748bc1e9fc37037bde8e1cf4d54" }, "downloads": -1, "filename": "toga-0.4.0.tar.gz", "has_sig": false, "md5_digest": "bd31edb40d9176b268250a6808b24d56", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.8", "size": 3815, "upload_time": "2023-11-03T04:09:40", "upload_time_iso_8601": "2023-11-03T04:09:40.090279Z", "url": "https://files.pythonhosted.org/packages/55/60/7dc01b43f991228fd73679e40dc000e4c28a76b8b27b4d155e5bf716ff06/toga-0.4.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/toga.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "01822b37a113268b35ff7f655cccc286926c96bff01a35b4884a116188e2fec3" }, "data-dist-info-metadata": { "sha256": "01822b37a113268b35ff7f655cccc286926c96bff01a35b4884a116188e2fec3" }, "filename": "toga-0.3.0.dev1-py3-none-any.whl", "hashes": { "sha256": "8553bf332d8fbf39b500745ed9c4044a846fbba68e31de70e6fe83fdffcb0a9e" }, "provenance": null, "requires-python": null, "size": 4789, "upload-time": "2018-01-14T04:10:57.825822Z", "url": "https://files.pythonhosted.org/packages/eb/ef/ea806c706d3dc90d4bc8c412c0ad3515fd018074f5fdd4bd020bdd4c0c80/toga-0.3.0.dev1-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "toga-0.3.0.dev1.tar.gz", "hashes": { "sha256": "4e5c77056792168a4e84c84bb7214dfb614b79f289dcbe1525be614483496439" }, "provenance": null, "requires-python": null, "size": 40187, "upload-time": "2018-01-14T04:11:03.212494Z", "url": "https://files.pythonhosted.org/packages/4f/71/c55c15950f7275e761fe53fb0dc83fe4f5fba6199d7b8fb05d741dd33566/toga-0.3.0.dev1.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "0d198a207170e3b5f2a7d20a8418efe297b77bb2ed098ece52c0366b2fc95de1" }, "data-dist-info-metadata": { "sha256": "0d198a207170e3b5f2a7d20a8418efe297b77bb2ed098ece52c0366b2fc95de1" }, "filename": "toga-0.3.0.dev2-py3-none-any.whl", "hashes": { "sha256": "6e0a2f800a351bbe8639802954d8d283a52b8cdde378541610ff2bfb3b24ad2f" }, "provenance": null, "requires-python": null, "size": 4788, "upload-time": "2018-01-14T04:52:03.658804Z", "url": "https://files.pythonhosted.org/packages/3a/68/d1f6feb2ded26b9f6c36cd2a826e895e0fa6bba5fe489ec30b9f3bc1dbea/toga-0.3.0.dev2-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "toga-0.3.0.dev2.tar.gz", "hashes": { "sha256": "630d2f932bf7aba3a143d3a332190a46a0a3895f509099a20f033caadf131b76" }, "provenance": null, "requires-python": null, "size": 40203, "upload-time": "2018-01-14T04:52:09.347038Z", "url": "https://files.pythonhosted.org/packages/e3/6a/3264b3d48733cac7546fee02fbc516621574252f7d86546255532b095415/toga-0.3.0.dev2.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "bec0f52a3d8b11f7b4fb7c2a944b889b0021628a7de577a3eedbd683f812707a" }, "data-dist-info-metadata": { "sha256": "bec0f52a3d8b11f7b4fb7c2a944b889b0021628a7de577a3eedbd683f812707a" }, "filename": "toga-0.3.0-py3-none-any.whl", "hashes": { "sha256": "846f1598d5ce91e44aefaac8abc9ba8c5a7c3bbd8c69d7004b92ef64228500ad" }, "provenance": null, "requires-python": ">=3.7", "size": 1945, "upload-time": "2023-01-30T03:29:31.034500Z", "url": "https://files.pythonhosted.org/packages/40/1d/f3f98e3811ef657aa2acabe039fcbbcae7fc3463f3e18a2458e099465a49/toga-0.3.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "toga-0.3.0.tar.gz", "hashes": { "sha256": "0d1f04d6b5773b8682e5eb1260ec9f07b43f950b89916f9406b2db33cde27240" }, "provenance": null, "requires-python": ">=3.7", "size": 3236, "upload-time": "2023-01-30T03:29:34.595566Z", "url": "https://files.pythonhosted.org/packages/f3/cf/44b2d505e13b2ec0c7cef2677ac5d8bffb1fb22c4c1d3671179f8a52a40b/toga-0.3.0.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "fe89f5e92f7709b1db6d8d865ac7a0a80e09154c26a83e692af163b78a332392" }, "data-dist-info-metadata": { "sha256": "fe89f5e92f7709b1db6d8d865ac7a0a80e09154c26a83e692af163b78a332392" }, "filename": "toga-0.4.0-py3-none-any.whl", "hashes": { "sha256": "cd2e47bb19ad7dfe0447f1379d4a7e8d9849a25ab8db62d41358185dcb4e4636" }, "provenance": null, "requires-python": ">=3.8", "size": 2270, "upload-time": "2023-11-03T04:09:37.311582Z", "url": "https://files.pythonhosted.org/packages/36/53/42de9ed8de0d256b785076ac7cf82eaa088f50df8ed2a8ab1011313e8f04/toga-0.4.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "toga-0.4.0.tar.gz", "hashes": { "sha256": "5ced4a0c85399a52e7c2397e7326f787a04da748bc1e9fc37037bde8e1cf4d54" }, "provenance": null, "requires-python": ">=3.8", "size": 3815, "upload-time": "2023-11-03T04:09:40.090279Z", "url": "https://files.pythonhosted.org/packages/55/60/7dc01b43f991228fd73679e40dc000e4c28a76b8b27b4d155e5bf716ff06/toga-0.4.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "toga", "project-status": { "status": "active" }, "versions": [ "0.3.0", "0.3.0dev1", "0.3.0dev2", "0.4.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/tomlkit/0.5.2.json ================================================ { "info": { "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/sdispater/tomlkit", "keywords": "", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "Sébastien Eustace", "maintainer_email": "sebastien@eustace.io", "name": "tomlkit", "package_url": "https://pypi.org/project/tomlkit/", "platform": "", "project_url": "https://pypi.org/project/tomlkit/", "project_urls": { "Homepage": "https://github.com/sdispater/tomlkit", "Repository": "https://github.com/sdispater/tomlkit" }, "provides_extra": null, "release_url": "https://pypi.org/project/tomlkit/0.5.2/", "requires_dist": [ "enum34 (>=1.1,<2.0); python_version >= \"2.7\" and python_version < \"2.8\"", "functools32 (>=3.2.3,<4.0.0); python_version >= \"2.7\" and python_version < \"2.8\"", "typing (>=3.6,<4.0); python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\"" ], "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "Style preserving TOML library", "version": "0.5.2", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "4045c5f6848fbc93c38df2296a441f07", "sha256": "dea8ff39e9e2170f1b2f465520482eec71e7909cfff53dcb076b585d50f8ccc8" }, "downloads": -1, "filename": "tomlkit-0.5.2-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "4045c5f6848fbc93c38df2296a441f07", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 116499, "upload_time": "2018-11-09T17:09:28", "upload_time_iso_8601": "2018-11-09T17:09:28.212157Z", "url": "https://files.pythonhosted.org/packages/9b/ca/8b60a94c01ee655ffb81d11c11396cb6fff89459317aa1fe3e98ee80f055/tomlkit-0.5.2-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "7c31987ef6fba2cd64715cae27fade64", "sha256": "4a226ccf11ee5a2e76bfc185747b54ee7718706aeb3aabb981327249dbe2b1d4" }, "downloads": -1, "filename": "tomlkit-0.5.2.tar.gz", "has_sig": false, "md5_digest": "7c31987ef6fba2cd64715cae27fade64", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 29813, "upload_time": "2018-11-09T17:09:29", "upload_time_iso_8601": "2018-11-09T17:09:29.709061Z", "url": "https://files.pythonhosted.org/packages/f6/8c/c27d292cf7c0f04f0e1b5c75ab95dc328542ccbe9a809a1eada66c897bd2/tomlkit-0.5.2.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/tomlkit/0.5.3.json ================================================ { "info": { "author": "Sébastien Eustace", "author_email": "sebastien@eustace.io", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/sdispater/tomlkit", "keywords": "", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "Sébastien Eustace", "maintainer_email": "sebastien@eustace.io", "name": "tomlkit", "package_url": "https://pypi.org/project/tomlkit/", "platform": "", "project_url": "https://pypi.org/project/tomlkit/", "project_urls": { "Homepage": "https://github.com/sdispater/tomlkit", "Repository": "https://github.com/sdispater/tomlkit" }, "provides_extra": null, "release_url": "https://pypi.org/project/tomlkit/0.5.3/", "requires_dist": [ "enum34 (>=1.1,<2.0); python_version >= \"2.7\" and python_version < \"2.8\"", "functools32 (>=3.2.3,<4.0.0); python_version >= \"2.7\" and python_version < \"2.8\"", "typing (>=3.6,<4.0); python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\"" ], "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "Style preserving TOML library", "version": "0.5.3", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "3a90c70a5067d5727110838094ab8674", "sha256": "35f06da5835e85f149a4701d43e730adcc09f1b362e5fc2300d77bdd26280908" }, "downloads": -1, "filename": "tomlkit-0.5.3-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "3a90c70a5067d5727110838094ab8674", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 116796, "upload_time": "2018-11-19T20:05:37", "upload_time_iso_8601": "2018-11-19T20:05:37.276181Z", "url": "https://files.pythonhosted.org/packages/71/c6/06c014b92cc48270765d6a9418d82239b158d8a9b69e031b0e2c6598740b/tomlkit-0.5.3-py2.py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "cdbdc302a184d1f1e38d5e0810e3b212", "sha256": "e2f785651609492c771d9887ccb2369d891d16595d2d97972e2cbe5e8fb3439f" }, "downloads": -1, "filename": "tomlkit-0.5.3.tar.gz", "has_sig": false, "md5_digest": "cdbdc302a184d1f1e38d5e0810e3b212", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 29864, "upload_time": "2018-11-19T20:05:39", "upload_time_iso_8601": "2018-11-19T20:05:39.200001Z", "url": "https://files.pythonhosted.org/packages/f7/f7/bbd9213bfe76cb7821c897f9ed74877fd74993b4ca2fe9513eb5a31030f9/tomlkit-0.5.3.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/tomlkit.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "4997b3d6f4b23bbd12d75af591167d982af7a862e473b2fbce20c8172ec93d93" }, "data-dist-info-metadata": { "sha256": "4997b3d6f4b23bbd12d75af591167d982af7a862e473b2fbce20c8172ec93d93" }, "filename": "tomlkit-0.5.2-py2.py3-none-any.whl", "hashes": { "md5": "4045c5f6848fbc93c38df2296a441f07", "sha256": "dea8ff39e9e2170f1b2f465520482eec71e7909cfff53dcb076b585d50f8ccc8" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 116499, "upload-time": "2018-11-09T17:09:28.212157Z", "url": "https://files.pythonhosted.org/packages/9b/ca/8b60a94c01ee655ffb81d11c11396cb6fff89459317aa1fe3e98ee80f055/tomlkit-0.5.2-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "tomlkit-0.5.2.tar.gz", "hashes": { "md5": "7c31987ef6fba2cd64715cae27fade64", "sha256": "4a226ccf11ee5a2e76bfc185747b54ee7718706aeb3aabb981327249dbe2b1d4" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 29813, "upload-time": "2018-11-09T17:09:29.709061Z", "url": "https://files.pythonhosted.org/packages/f6/8c/c27d292cf7c0f04f0e1b5c75ab95dc328542ccbe9a809a1eada66c897bd2/tomlkit-0.5.2.tar.gz", "yanked": false }, { "core-metadata": { "sha256": "729d3b14964e2455c0082529dc24dce1c6c84dc74f76d14b5ce7a0f044b12bbe" }, "data-dist-info-metadata": { "sha256": "729d3b14964e2455c0082529dc24dce1c6c84dc74f76d14b5ce7a0f044b12bbe" }, "filename": "tomlkit-0.5.3-py2.py3-none-any.whl", "hashes": { "md5": "3a90c70a5067d5727110838094ab8674", "sha256": "35f06da5835e85f149a4701d43e730adcc09f1b362e5fc2300d77bdd26280908" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 116796, "upload-time": "2018-11-19T20:05:37.276181Z", "url": "https://files.pythonhosted.org/packages/71/c6/06c014b92cc48270765d6a9418d82239b158d8a9b69e031b0e2c6598740b/tomlkit-0.5.3-py2.py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "tomlkit-0.5.3.tar.gz", "hashes": { "md5": "cdbdc302a184d1f1e38d5e0810e3b212", "sha256": "e2f785651609492c771d9887ccb2369d891d16595d2d97972e2cbe5e8fb3439f" }, "provenance": null, "requires-python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 29864, "upload-time": "2018-11-19T20:05:39.200001Z", "url": "https://files.pythonhosted.org/packages/f7/f7/bbd9213bfe76cb7821c897f9ed74877fd74993b4ca2fe9513eb5a31030f9/tomlkit-0.5.3.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "tomlkit", "project-status": { "status": "active" }, "versions": [ "0.5.2", "0.5.3" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/twisted/18.9.0.json ================================================ { "info": { "author": "Twisted Matrix Laboratories", "author_email": "twisted-python@twistedmatrix.com", "bugtrack_url": null, "classifiers": [ "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "http://twistedmatrix.com/", "keywords": "", "license": "MIT", "license_expression": null, "license_files": null, "maintainer": "Glyph Lefkowitz", "maintainer_email": "glyph@twistedmatrix.com", "name": "Twisted", "package_url": "https://pypi.org/project/Twisted/", "platform": "", "project_url": "https://pypi.org/project/Twisted/", "project_urls": { "Homepage": "http://twistedmatrix.com/" }, "provides_extra": null, "release_url": "https://pypi.org/project/Twisted/18.9.0/", "requires_dist": null, "requires_python": "", "summary": "An asynchronous networking framework written in Python", "version": "18.9.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "35ff4705ea90a76bf972ff3b229546ca", "sha256": "4335327da58be11dd6e482ec6b85eb055bcc953a9570cd59e7840a2ce9419a8e" }, "downloads": -1, "filename": "Twisted-18.9.0.tar.bz2", "has_sig": false, "md5_digest": "35ff4705ea90a76bf972ff3b229546ca", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 3088398, "upload_time": "2018-10-15T09:11:22", "upload_time_iso_8601": "2018-10-15T09:11:22.298247Z", "url": "https://files.pythonhosted.org/packages/5d/0e/a72d85a55761c2c3ff1cb968143a2fd5f360220779ed90e0fadf4106d4f2/Twisted-18.9.0.tar.bz2", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/twisted.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "Twisted-18.9.0.tar.bz2", "hashes": { "md5": "35ff4705ea90a76bf972ff3b229546ca", "sha256": "4335327da58be11dd6e482ec6b85eb055bcc953a9570cd59e7840a2ce9419a8e" }, "provenance": null, "requires-python": null, "size": 3088398, "upload-time": "2018-10-15T09:11:22.298247Z", "url": "https://files.pythonhosted.org/packages/5d/0e/a72d85a55761c2c3ff1cb968143a2fd5f360220779ed90e0fadf4106d4f2/Twisted-18.9.0.tar.bz2", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "twisted", "project-status": { "status": "active" }, "versions": [ "18.9.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/wheel/0.40.0.json ================================================ { "info": { "author": "", "author_email": "Daniel Holth ", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: System :: Archiving :: Packaging" ], "description": "", "description_content_type": "text/x-rst", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "", "keywords": "wheel,packaging", "license": "", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "Alex Grönholm ", "name": "wheel", "package_url": "https://pypi.org/project/wheel/", "platform": null, "project_url": "https://pypi.org/project/wheel/", "project_urls": { "Changelog": "https://wheel.readthedocs.io/en/stable/news.html", "Documentation": "https://wheel.readthedocs.io/", "Issue Tracker": "https://github.com/pypa/wheel/issues" }, "provides_extra": null, "release_url": "https://pypi.org/project/wheel/0.40.0/", "requires_dist": [ "pytest >= 6.0.0 ; extra == \"test\"" ], "requires_python": ">=3.7", "summary": "A built-package format for Python", "version": "0.40.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "517d39f133bd7b1ff17caf09784b7543", "sha256": "d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247" }, "downloads": -1, "filename": "wheel-0.40.0-py3-none-any.whl", "has_sig": false, "md5_digest": "517d39f133bd7b1ff17caf09784b7543", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 64545, "upload_time": "2023-03-14T15:10:00", "upload_time_iso_8601": "2023-03-14T15:10:00.828550Z", "url": "https://files.pythonhosted.org/packages/61/86/cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5/wheel-0.40.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "5f175a8d693f74878964d4fd29729ab7", "sha256": "5cb7e75751aa82e1b7db3fd52f5a9d59e7b06905630bed135793295931528740" }, "downloads": -1, "filename": "wheel-0.40.0.tar.gz", "has_sig": false, "md5_digest": "5f175a8d693f74878964d4fd29729ab7", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 96226, "upload_time": "2023-03-14T15:10:02", "upload_time_iso_8601": "2023-03-14T15:10:02.873691Z", "url": "https://files.pythonhosted.org/packages/fc/ef/0335f7217dd1e8096a9e8383e1d472aa14717878ffe07c4772e68b6e8735/wheel-0.40.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/wheel.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "bf7a632f73a2d57bb54d06bbb2a46c162e6dfbe7565b0746e588ce6f080e49f7" }, "data-dist-info-metadata": { "sha256": "bf7a632f73a2d57bb54d06bbb2a46c162e6dfbe7565b0746e588ce6f080e49f7" }, "filename": "wheel-0.40.0-py3-none-any.whl", "hashes": { "md5": "517d39f133bd7b1ff17caf09784b7543", "sha256": "d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247" }, "provenance": null, "requires-python": ">=3.7", "size": 64545, "upload-time": "2023-03-14T15:10:00.828550Z", "url": "https://files.pythonhosted.org/packages/61/86/cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5/wheel-0.40.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "wheel-0.40.0.tar.gz", "hashes": { "md5": "5f175a8d693f74878964d4fd29729ab7", "sha256": "5cb7e75751aa82e1b7db3fd52f5a9d59e7b06905630bed135793295931528740" }, "provenance": null, "requires-python": ">=3.7", "size": 96226, "upload-time": "2023-03-14T15:10:02.873691Z", "url": "https://files.pythonhosted.org/packages/fc/ef/0335f7217dd1e8096a9e8383e1d472aa14717878ffe07c4772e68b6e8735/wheel-0.40.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "wheel", "project-status": { "status": "active" }, "versions": [ "0.40.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/zipp/3.5.0.json ================================================ { "info": { "author": "Jason R. Coombs", "author_email": "jaraco@jaraco.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only" ], "description": "", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "dynamic": null, "home_page": "https://github.com/jaraco/zipp", "keywords": "", "license": "", "license_expression": null, "license_files": null, "maintainer": "", "maintainer_email": "", "name": "zipp", "package_url": "https://pypi.org/project/zipp/", "platform": "", "project_url": "https://pypi.org/project/zipp/", "project_urls": { "Homepage": "https://github.com/jaraco/zipp" }, "provides_extra": null, "release_url": "https://pypi.org/project/zipp/3.5.0/", "requires_dist": [ "sphinx ; extra == 'docs'", "jaraco.packaging (>=8.2) ; extra == 'docs'", "rst.linker (>=1.9) ; extra == 'docs'", "pytest (>=4.6) ; extra == 'testing'", "pytest-checkdocs (>=2.4) ; extra == 'testing'", "pytest-flake8 ; extra == 'testing'", "pytest-cov ; extra == 'testing'", "pytest-enabler (>=1.0.1) ; extra == 'testing'", "jaraco.itertools ; extra == 'testing'", "func-timeout ; extra == 'testing'", "pytest-black (>=0.3.7) ; (platform_python_implementation != \"PyPy\" and python_version < \"3.10\") and extra == 'testing'", "pytest-mypy ; (platform_python_implementation != \"PyPy\" and python_version < \"3.10\") and extra == 'testing'" ], "requires_python": ">=3.6", "summary": "Backport of pathlib-compatible object wrapper for zip files", "version": "3.5.0", "yanked": false, "yanked_reason": null }, "last_serial": 0, "urls": [ { "comment_text": "", "digests": { "md5": "da62cbd850ba32ba93817aab0f03a855", "sha256": "ec508cd5a3ed3d126293cafb34611469f2aef7342f575c3b6e072b995dc9da1f" }, "downloads": -1, "filename": "zipp-3.5.0-py3-none-any.whl", "has_sig": false, "md5_digest": "da62cbd850ba32ba93817aab0f03a855", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 5700, "upload_time": "2021-07-02T23:51:45", "upload_time_iso_8601": "2021-07-02T23:51:45.759726Z", "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "16bf2a24fae340052e8565c264d21092", "sha256": "239d50954a15aa4b283023f18dc451ba811fb4d263f4dd6855642e4d1c80cc9f" }, "downloads": -1, "filename": "zipp-3.5.0.tar.gz", "has_sig": false, "md5_digest": "16bf2a24fae340052e8565c264d21092", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 13270, "upload_time": "2021-07-02T23:51:47", "upload_time_iso_8601": "2021-07-02T23:51:47.004396Z", "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] } ================================================ FILE: tests/repositories/fixtures/pypi.org/json/zipp.json ================================================ { "alternate-locations": [], "files": [ { "core-metadata": { "sha256": "03c98c88c78ef3602e6fd904a321359fa0e374bee5d9e5bf3b1fbdecc3c8a92d" }, "data-dist-info-metadata": { "sha256": "03c98c88c78ef3602e6fd904a321359fa0e374bee5d9e5bf3b1fbdecc3c8a92d" }, "filename": "zipp-3.5.0-py3-none-any.whl", "hashes": { "md5": "da62cbd850ba32ba93817aab0f03a855", "sha256": "ec508cd5a3ed3d126293cafb34611469f2aef7342f575c3b6e072b995dc9da1f" }, "provenance": null, "requires-python": ">=3.6", "size": 5700, "upload-time": "2021-07-02T23:51:45.759726Z", "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "zipp-3.5.0.tar.gz", "hashes": { "md5": "16bf2a24fae340052e8565c264d21092", "sha256": "239d50954a15aa4b283023f18dc451ba811fb4d263f4dd6855642e4d1c80cc9f" }, "provenance": null, "requires-python": ">=3.6", "size": 13270, "upload-time": "2021-07-02T23:51:47.004396Z", "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz", "yanked": false } ], "meta": { "_last-serial": 0, "api-version": "1.4" }, "name": "zipp", "project-status": { "status": "active" }, "versions": [ "3.5.0" ] } ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp27-cp27m-win32.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp27-cp27m-win_amd64.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp34-cp34m-win32.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp34-cp34m-win_amd64.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp35-cp35m-win32.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp35-cp35m-win_amd64.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp36-cp36m-win32.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp36-cp36m-win_amd64.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp37-cp37m-win32.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/PyYAML-3.13-cp37-cp37m-win_amd64.whl.metadata ================================================ Metadata-Version: 2.1 Name: PyYAML Version: 3.13 Summary: YAML parser and emitter for Python Home-page: http://pyyaml.org/wiki/PyYAML Author: Kirill Simonov Author-email: xi@resolvent.net License: MIT Download-URL: http://pyyaml.org/download/pyyaml/PyYAML-3.13.tar.gz Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/attrs-17.4.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: attrs Version: 17.4.0 Summary: Classes Without Boilerplate Home-page: http://www.attrs.org/ Author: Hynek Schlawack Author-email: hs@ox.cx License: MIT Description-Content-Type: UNKNOWN Keywords: class,attribute,boilerplate Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides-Extra: dev Requires-Dist: coverage; extra == 'dev' Requires-Dist: hypothesis; extra == 'dev' Requires-Dist: pympler; extra == 'dev' Requires-Dist: pytest; extra == 'dev' Requires-Dist: six; extra == 'dev' Requires-Dist: zope.interface; extra == 'dev' Requires-Dist: sphinx; extra == 'dev' Requires-Dist: zope.interface; extra == 'dev' Provides-Extra: docs Requires-Dist: sphinx; extra == 'docs' Requires-Dist: zope.interface; extra == 'docs' Provides-Extra: tests Requires-Dist: coverage; extra == 'tests' Requires-Dist: hypothesis; extra == 'tests' Requires-Dist: pympler; extra == 'tests' Requires-Dist: pytest; extra == 'tests' Requires-Dist: six; extra == 'tests' Requires-Dist: zope.interface; extra == 'tests' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/black-19.10b0-py36-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: black Version: 19.10b0 Summary: The uncompromising code formatter. Home-page: https://github.com/psf/black Author: Łukasz Langa Author-email: lukasz@langa.pl License: MIT Keywords: automation formatter yapf autopep8 pyfmt gofmt rustfmt Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Quality Assurance Requires-Python: >=3.6 Description-Content-Type: text/markdown Requires-Dist: click (>=6.5) Requires-Dist: attrs (>=18.1.0) Requires-Dist: appdirs Requires-Dist: toml (>=0.9.4) Requires-Dist: typed-ast (>=1.4.0) Requires-Dist: regex Requires-Dist: pathspec (<1,>=0.6) Provides-Extra: d Requires-Dist: aiohttp (>=3.3.2) ; extra == 'd' Requires-Dist: aiohttp-cors ; extra == 'd' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/black-21.11b0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: black Version: 21.11b0 Summary: The uncompromising code formatter. Home-page: https://github.com/psf/black Author: Łukasz Langa Author-email: lukasz@langa.pl License: MIT Project-URL: Changelog, https://github.com/psf/black/blob/main/CHANGES.md Keywords: automation formatter yapf autopep8 pyfmt gofmt rustfmt Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Quality Assurance Requires-Python: >=3.6.2 Description-Content-Type: text/markdown License-File: LICENSE License-File: AUTHORS.md Requires-Dist: click (>=7.1.2) Requires-Dist: platformdirs (>=2) Requires-Dist: tomli (<2.0.0,>=0.2.6) Requires-Dist: regex (>=2020.1.8) Requires-Dist: pathspec (<1,>=0.9.0) Requires-Dist: typing-extensions (>=3.10.0.0) Requires-Dist: mypy-extensions (>=0.4.3) Requires-Dist: dataclasses (>=0.6) ; python_version < "3.7" Requires-Dist: typed-ast (>=1.4.2) ; python_version < "3.8" and implementation_name == "cpython" Requires-Dist: typing-extensions (!=3.10.0.1) ; python_version >= "3.10" Provides-Extra: colorama Requires-Dist: colorama (>=0.4.3) ; extra == 'colorama' Provides-Extra: d Requires-Dist: aiohttp (>=3.7.4) ; extra == 'd' Provides-Extra: jupyter Requires-Dist: ipython (>=7.8.0) ; extra == 'jupyter' Requires-Dist: tokenize-rt (>=3.2.0) ; extra == 'jupyter' Provides-Extra: python2 Requires-Dist: typed-ast (>=1.4.3) ; extra == 'python2' Provides-Extra: uvloop Requires-Dist: uvloop (>=0.15.2) ; extra == 'uvloop' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/cleo-1.0.0a5-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: cleo Version: 1.0.0a5 Summary: Cleo allows you to create beautiful and testable command-line interfaces. Home-page: https://github.com/python-poetry/cleo License: MIT Keywords: cli,commands Author: Sébastien Eustace Author-email: sebastien@eustace.io Requires-Python: >=3.7,<4.0 Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Requires-Dist: crashtest (>=0.3.1,<0.4.0) Requires-Dist: pylev (>=1.3.0,<2.0.0) Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/clikit-0.2.4-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: clikit Version: 0.2.4 Summary: CliKit is a group of utilities to build beautiful and testable command line interfaces. Home-page: https://github.com/sdispater/clikit License: MIT Keywords: packaging,dependency,poetry Author: Sébastien Eustace Author-email: sebastien@eustace.io Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Requires-Dist: enum34 (>=1.1,<2.0); python_version >= "2.7" and python_version < "2.8" Requires-Dist: pastel (>=0.1.0,<0.2.0) Requires-Dist: pylev (>=1.3,<2.0) Requires-Dist: typing (>=3.6,<4.0); python_version >= "2.7" and python_version < "2.8" or python_version >= "3.4" and python_version < "3.5" Project-URL: Repository, https://github.com/sdispater/clikit Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/colorama-0.3.9-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: colorama Version: 0.3.9 Summary: Cross-platform colored terminal text. Home-page: https://github.com/tartley/colorama Author: Arnon Yaari Author-email: tartley@tartley.com License: BSD Keywords: color colour terminal text ansi windows crossplatform xplatform Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.1 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Terminals ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/discord.py-2.0.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: discord.py Version: 2.0.0 Summary: A Python wrapper for the Discord API Home-page: https://github.com/Rapptz/discord.py Author: Rapptz License: MIT Project-URL: Documentation, https://discordpy.readthedocs.io/en/latest/ Project-URL: Issue tracker, https://github.com/Rapptz/discord.py/issues Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: MIT License Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Internet Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Classifier: Typing :: Typed Requires-Python: >=3.8.0 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: aiohttp (<4,>=3.7.4) Provides-Extra: docs Requires-Dist: sphinx (==4.4.0) ; extra == 'docs' Requires-Dist: sphinxcontrib-trio (==1.1.2) ; extra == 'docs' Requires-Dist: sphinxcontrib-websupport ; extra == 'docs' Requires-Dist: typing-extensions (<5,>=4.3) ; extra == 'docs' Provides-Extra: speed Requires-Dist: orjson (>=3.5.4) ; extra == 'speed' Requires-Dist: aiodns (>=1.1) ; extra == 'speed' Requires-Dist: Brotli ; extra == 'speed' Requires-Dist: cchardet ; extra == 'speed' Provides-Extra: test Requires-Dist: coverage[toml] ; extra == 'test' Requires-Dist: pytest ; extra == 'test' Requires-Dist: pytest-asyncio ; extra == 'test' Requires-Dist: pytest-cov ; extra == 'test' Requires-Dist: pytest-mock ; extra == 'test' Requires-Dist: typing-extensions (<5,>=4.3) ; extra == 'test' Provides-Extra: voice Requires-Dist: PyNaCl (<1.6,>=1.3.0) ; extra == 'voice' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/filecache-0.81-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: filecache Version: 0.81 Summary: Persistent caching decorator Home-page: https://github.com/ubershmekel/filecache Author: ubershmekel Author-email: ubershmekel@gmail.com License: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Utilities Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/funcsigs-1.0.2-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: funcsigs Version: 1.0.2 Summary: Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+ Home-page: http://funcsigs.readthedocs.org Author: Testing Cabal Author-email: testing-in-python@lists.idyll.org License: ASL Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Dist: ordereddict; python_version<"2.7" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/futures-3.2.0-py2-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: futures Version: 3.2.0 Summary: Backport of the concurrent.futures package from Python 3 Home-page: https://github.com/agronholm/pythonfutures Author: Alex Grönholm Author-email: alex.gronholm@nextday.fi License: PSF Platform: UNKNOWN Classifier: License :: OSI Approved :: Python Software Foundation License Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 2 :: Only Requires-Python: >=2.6, <3 ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/importlib_metadata-1.7.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: importlib-metadata Version: 1.7.0 Summary: Read metadata from Python packages Home-page: http://importlib-metadata.readthedocs.io/ Author: Barry Warsaw Author-email: barry@python.org License: Apache Software License Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Topic :: Software Development :: Libraries Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 2 Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 Requires-Dist: zipp (>=0.5) Requires-Dist: pathlib2 ; python_version < "3" Requires-Dist: contextlib2 ; python_version < "3" Requires-Dist: configparser (>=3.5) ; python_version < "3" Provides-Extra: docs Requires-Dist: sphinx ; extra == 'docs' Requires-Dist: rst.linker ; extra == 'docs' Provides-Extra: testing Requires-Dist: packaging ; extra == 'testing' Requires-Dist: pep517 ; extra == 'testing' Requires-Dist: importlib-resources (>=1.3) ; (python_version < "3.9") and extra == 'testing' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/ipython-4.1.0rc1-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: ipython Version: 4.1.0rc1 Summary: IPython: Productive Interactive Computing Home-page: http://ipython.org Author: The IPython Development Team Author-email: ipython-dev@scipy.org License: BSD Download-URL: https://github.com/ipython/ipython/downloads Keywords: Interactive,Interpreter,Shell,Parallel,Distributed,Web-based computing,Qt console,Embedding Platform: Linux Platform: Mac OSX Platform: Windows XP/Vista/7/8 Classifier: Framework :: IPython Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: System :: Shells Requires-Dist: pickleshare Requires-Dist: setuptools (>=18.5decorator) Requires-Dist: simplegeneric (>0.8) Requires-Dist: traitlets Requires-Dist: pexpect; sys_platform != "win32" Requires-Dist: appnope; sys_platform == "darwin" Requires-Dist: gnureadline; sys_platform == "darwin" and platform_python_implementation == "CPython" Provides-Extra: all Requires-Dist: Sphinx (>=1.3); extra == 'all' Requires-Dist: ipykernel; extra == 'all' Requires-Dist: ipyparallel; extra == 'all' Requires-Dist: ipywidgets; extra == 'all' Requires-Dist: nbconvert; extra == 'all' Requires-Dist: nbformat; extra == 'all' Requires-Dist: nose (>=0.10.1); extra == 'all' Requires-Dist: notebook; extra == 'all' Requires-Dist: qtconsole; extra == 'all' Requires-Dist: requests; extra == 'all' Requires-Dist: testpath; extra == 'all' Provides-Extra: doc Requires-Dist: Sphinx (>=1.3); extra == 'doc' Provides-Extra: kernel Requires-Dist: ipykernel; extra == 'kernel' Provides-Extra: nbconvert Requires-Dist: nbconvert; extra == 'nbconvert' Provides-Extra: nbformat Requires-Dist: nbformat; extra == 'nbformat' Provides-Extra: notebook Requires-Dist: ipywidgets; extra == 'notebook' Requires-Dist: notebook; extra == 'notebook' Provides-Extra: parallel Requires-Dist: ipyparallel; extra == 'parallel' Provides-Extra: qtconsole Requires-Dist: qtconsole; extra == 'qtconsole' Provides-Extra: terminal Provides-Extra: terminal Requires-Dist: pyreadline (>=2); sys_platform == "win32" and extra == 'terminal' Provides-Extra: test Requires-Dist: nose (>=0.10.1); extra == 'test' Requires-Dist: requests; extra == 'test' Requires-Dist: testpath; extra == 'test' Provides-Extra: test Requires-Dist: mock; python_version == "2.7" and extra == 'test' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/ipython-5.7.0-py2-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: ipython Version: 5.7.0 Summary: IPython: Productive Interactive Computing Home-page: https://ipython.org Author: The IPython Development Team Author-email: ipython-dev@python.org License: BSD Description-Content-Type: UNKNOWN Keywords: Interactive,Interpreter,Shell,Embedding Platform: Linux Platform: Mac OSX Platform: Windows Classifier: Framework :: IPython Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: System :: Shells Requires-Dist: setuptools (>=18.5) Requires-Dist: decorator Requires-Dist: pickleshare Requires-Dist: simplegeneric (>0.8) Requires-Dist: traitlets (>=4.2) Requires-Dist: prompt-toolkit (<2.0.0,>=1.0.4) Requires-Dist: pygments Requires-Dist: backports.shutil-get-terminal-size; python_version == "2.7" Requires-Dist: pathlib2; python_version == "2.7" or python_version == "3.3" Requires-Dist: pexpect; sys_platform != "win32" Requires-Dist: appnope; sys_platform == "darwin" Requires-Dist: colorama; sys_platform == "win32" Requires-Dist: win-unicode-console (>=0.5); sys_platform == "win32" and python_version < "3.6" Provides-Extra: all Requires-Dist: nbformat; extra == 'all' Requires-Dist: ipykernel; extra == 'all' Requires-Dist: pygments; extra == 'all' Requires-Dist: testpath; extra == 'all' Requires-Dist: notebook; extra == 'all' Requires-Dist: nbconvert; extra == 'all' Requires-Dist: ipyparallel; extra == 'all' Requires-Dist: qtconsole; extra == 'all' Requires-Dist: Sphinx (>=1.3); extra == 'all' Requires-Dist: requests; extra == 'all' Requires-Dist: nose (>=0.10.1); extra == 'all' Requires-Dist: ipywidgets; extra == 'all' Provides-Extra: doc Requires-Dist: Sphinx (>=1.3); extra == 'doc' Provides-Extra: kernel Requires-Dist: ipykernel; extra == 'kernel' Provides-Extra: nbconvert Requires-Dist: nbconvert; extra == 'nbconvert' Provides-Extra: nbformat Requires-Dist: nbformat; extra == 'nbformat' Provides-Extra: notebook Requires-Dist: notebook; extra == 'notebook' Requires-Dist: ipywidgets; extra == 'notebook' Provides-Extra: parallel Requires-Dist: ipyparallel; extra == 'parallel' Provides-Extra: qtconsole Requires-Dist: qtconsole; extra == 'qtconsole' Provides-Extra: terminal Provides-Extra: test Requires-Dist: nose (>=0.10.1); extra == 'test' Requires-Dist: requests; extra == 'test' Requires-Dist: testpath; extra == 'test' Requires-Dist: pygments; extra == 'test' Requires-Dist: nbformat; extra == 'test' Requires-Dist: ipykernel; extra == 'test' Provides-Extra: test Requires-Dist: mock; python_version == "2.7" and extra == 'test' Provides-Extra: test Requires-Dist: numpy; python_version >= "3.4" and extra == 'test' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/ipython-5.7.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: ipython Version: 5.7.0 Summary: IPython: Productive Interactive Computing Home-page: https://ipython.org Author: The IPython Development Team Author-email: ipython-dev@python.org License: BSD Description-Content-Type: UNKNOWN Keywords: Interactive,Interpreter,Shell,Embedding Platform: Linux Platform: Mac OSX Platform: Windows Classifier: Framework :: IPython Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: System :: Shells Requires-Dist: setuptools (>=18.5) Requires-Dist: decorator Requires-Dist: pickleshare Requires-Dist: simplegeneric (>0.8) Requires-Dist: traitlets (>=4.2) Requires-Dist: prompt-toolkit (<2.0.0,>=1.0.4) Requires-Dist: pygments Requires-Dist: backports.shutil-get-terminal-size; python_version == "2.7" Requires-Dist: pathlib2; python_version == "2.7" or python_version == "3.3" Requires-Dist: pexpect; sys_platform != "win32" Requires-Dist: appnope; sys_platform == "darwin" Requires-Dist: colorama; sys_platform == "win32" Requires-Dist: win-unicode-console (>=0.5); sys_platform == "win32" and python_version < "3.6" Provides-Extra: all Requires-Dist: nose (>=0.10.1); extra == 'all' Requires-Dist: ipywidgets; extra == 'all' Requires-Dist: nbformat; extra == 'all' Requires-Dist: pygments; extra == 'all' Requires-Dist: ipyparallel; extra == 'all' Requires-Dist: requests; extra == 'all' Requires-Dist: ipykernel; extra == 'all' Requires-Dist: qtconsole; extra == 'all' Requires-Dist: testpath; extra == 'all' Requires-Dist: nbconvert; extra == 'all' Requires-Dist: Sphinx (>=1.3); extra == 'all' Requires-Dist: notebook; extra == 'all' Provides-Extra: doc Requires-Dist: Sphinx (>=1.3); extra == 'doc' Provides-Extra: kernel Requires-Dist: ipykernel; extra == 'kernel' Provides-Extra: nbconvert Requires-Dist: nbconvert; extra == 'nbconvert' Provides-Extra: nbformat Requires-Dist: nbformat; extra == 'nbformat' Provides-Extra: notebook Requires-Dist: notebook; extra == 'notebook' Requires-Dist: ipywidgets; extra == 'notebook' Provides-Extra: parallel Requires-Dist: ipyparallel; extra == 'parallel' Provides-Extra: qtconsole Requires-Dist: qtconsole; extra == 'qtconsole' Provides-Extra: terminal Provides-Extra: test Requires-Dist: nose (>=0.10.1); extra == 'test' Requires-Dist: requests; extra == 'test' Requires-Dist: testpath; extra == 'test' Requires-Dist: pygments; extra == 'test' Requires-Dist: nbformat; extra == 'test' Requires-Dist: ipykernel; extra == 'test' Provides-Extra: test Requires-Dist: mock; python_version == "2.7" and extra == 'test' Provides-Extra: test Requires-Dist: numpy; python_version >= "3.4" and extra == 'test' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/ipython-7.5.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: ipython Version: 7.5.0 Summary: IPython: Productive Interactive Computing Home-page: https://ipython.org Author: The IPython Development Team Author-email: ipython-dev@python.org License: BSD Project-URL: Documentation, https://ipython.readthedocs.io/ Project-URL: Funding, https://numfocus.org/ Project-URL: Source, https://github.com/ipython/ipython Project-URL: Tracker, https://github.com/ipython/ipython/issues Keywords: Interactive,Interpreter,Shell,Embedding Platform: Linux Platform: Mac OSX Platform: Windows Classifier: Framework :: IPython Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: System :: Shells Requires-Python: >=3.5 Provides-Extra: parallel Provides-Extra: nbconvert Provides-Extra: terminal Provides-Extra: kernel Provides-Extra: notebook Provides-Extra: doc Provides-Extra: nbformat Provides-Extra: test Provides-Extra: all Provides-Extra: qtconsole Requires-Dist: setuptools (>=18.5) Requires-Dist: jedi (>=0.10) Requires-Dist: decorator Requires-Dist: pickleshare Requires-Dist: traitlets (>=4.2) Requires-Dist: prompt-toolkit (<2.1.0,>=2.0.0) Requires-Dist: pygments Requires-Dist: backcall Requires-Dist: typing; python_version == "3.4" Requires-Dist: pexpect; sys_platform != "win32" Requires-Dist: appnope; sys_platform == "darwin" Requires-Dist: colorama; sys_platform == "win32" Requires-Dist: win-unicode-console (>=0.5); sys_platform == "win32" and python_version < "3.6" Provides-Extra: all Requires-Dist: nbconvert; extra == 'all' Requires-Dist: ipywidgets; extra == 'all' Requires-Dist: pygments; extra == 'all' Requires-Dist: ipykernel; extra == 'all' Requires-Dist: notebook; extra == 'all' Requires-Dist: ipyparallel; extra == 'all' Requires-Dist: requests; extra == 'all' Requires-Dist: Sphinx (>=1.3); extra == 'all' Requires-Dist: nbformat; extra == 'all' Requires-Dist: nose (>=0.10.1); extra == 'all' Requires-Dist: numpy; extra == 'all' Requires-Dist: testpath; extra == 'all' Requires-Dist: qtconsole; extra == 'all' Provides-Extra: doc Requires-Dist: Sphinx (>=1.3); extra == 'doc' Provides-Extra: kernel Requires-Dist: ipykernel; extra == 'kernel' Provides-Extra: nbconvert Requires-Dist: nbconvert; extra == 'nbconvert' Provides-Extra: nbformat Requires-Dist: nbformat; extra == 'nbformat' Provides-Extra: notebook Requires-Dist: notebook; extra == 'notebook' Requires-Dist: ipywidgets; extra == 'notebook' Provides-Extra: parallel Requires-Dist: ipyparallel; extra == 'parallel' Provides-Extra: qtconsole Requires-Dist: qtconsole; extra == 'qtconsole' Provides-Extra: terminal Provides-Extra: test Requires-Dist: nose (>=0.10.1); extra == 'test' Requires-Dist: requests; extra == 'test' Requires-Dist: testpath; extra == 'test' Requires-Dist: pygments; extra == 'test' Requires-Dist: nbformat; extra == 'test' Requires-Dist: ipykernel; extra == 'test' Requires-Dist: numpy; extra == 'test' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/isodate-0.7.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: isodate Version: 0.7.0 Summary: An ISO 8601 date/time/duration parser and formatter Author: Gerhard Weis License: Copyright (c) 2021, Hugo van Kemenade and contributors Copyright (c) 2009-2018, Gerhard Weis and contributors Copyright (c) 2009, Gerhard Weis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Project-URL: Homepage, https://github.com/gweis/isodate/ Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/x-rst License-File: LICENSE ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/isort-4.3.4-py2-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: isort Version: 4.3.4 Summary: A Python utility / library to sort Python imports. Home-page: https://github.com/timothycrosley/isort Author: Timothy Crosley Author-email: timothy.crosley@gmail.com License: MIT Description-Content-Type: UNKNOWN Keywords: Refactor,Python,Python2,Python3,Refactoring,Imports,Sort,Clean Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Environment :: Console Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Requires-Dist: futures Requires-Dist: futures; python_version=="2.7" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/isort-4.3.4-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: isort Version: 4.3.4 Summary: A Python utility / library to sort Python imports. Home-page: https://github.com/timothycrosley/isort Author: Timothy Crosley Author-email: timothy.crosley@gmail.com License: MIT Keywords: Refactor,Python,Python2,Python3,Refactoring,Imports,Sort,Clean Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Environment :: Console Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Requires-Dist: futures; python_version=="2.7" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/isort-metadata-4.3.4-py2-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: isort-metadata Version: 4.3.4 Summary: A Python utility / library to sort Python imports. Home-page: https://github.com/timothycrosley/isort Author: Timothy Crosley Author-email: timothy.crosley@gmail.com License: MIT Keywords: Refactor,Python,Python2,Python3,Refactoring,Imports,Sort,Clean Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Environment :: Console Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Requires-Dist: futures; python_version=="2.7" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/isort-metadata-4.3.4-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: isort-metadata Version: 4.3.4 Summary: A Python utility / library to sort Python imports. Home-page: https://github.com/timothycrosley/isort Author: Timothy Crosley Author-email: timothy.crosley@gmail.com License: MIT Keywords: Refactor,Python,Python2,Python3,Refactoring,Imports,Sort,Clean Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Environment :: Console Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Requires-Dist: futures; python_version=="2.7" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/jupyter-1.0.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: jupyter Version: 1.0.0 Summary: Jupyter metapackage. Install all the Jupyter components in one go. Home-page: http://jupyter.org Author: Jupyter Development Team Author-email: jupyter@googlegroups.org License: BSD Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Requires-Dist: notebook Requires-Dist: qtconsole Requires-Dist: jupyter-console Requires-Dist: nbconvert Requires-Dist: ipykernel Requires-Dist: ipywidgets ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/mocked/with_extra_dependency-0.12.4-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: with-extra-dependency Version: 0.12.4 Summary: Mock Package Home-page: https://python-poetry.org/ License: MIT Keywords: packaging,dependency,poetry Author: Poetry Team Author-email: noreply@python-poetry.org Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Dist: filecache; extra == 'filecache' Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/mocked/with_transitive_extra_dependency-0.12.4-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: with-transitive-extra-dependency Version: 0.12.4 Summary: Mock Package Home-page: https://python-poetry.org/ License: MIT Keywords: packaging,dependency,poetry Author: Poetry Team Author-email: noreply@python-poetry.org Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Dist: with-extra-dependency[filecache] (>=0.12.4,<0.13.0) Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/more_itertools-4.1.0-py2-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: more-itertools Version: 4.1.0 Summary: More routines for operating on iterables, beyond itertools Home-page: https://github.com/erikrose/more-itertools Author: Erik Rose Author-email: erikrose@grinchcentral.com License: MIT Description-Content-Type: UNKNOWN Keywords: itertools,iterator,iteration,filter,peek,peekable,collate,chunk,chunked Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries Requires-Dist: six (<2.0.0,>=1.0.0) ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/more_itertools-4.1.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: more-itertools Version: 4.1.0 Summary: More routines for operating on iterables, beyond itertools Home-page: https://github.com/erikrose/more-itertools Author: Erik Rose Author-email: erikrose@grinchcentral.com License: MIT Description-Content-Type: UNKNOWN Keywords: itertools,iterator,iteration,filter,peek,peekable,collate,chunk,chunked Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries Requires-Dist: six (<2.0.0,>=1.0.0) ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/pastel-0.1.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: pastel Version: 0.1.0 Summary: Bring colors to your terminal. Home-page: https://github.com/sdispater/pastel Author: Sébastien Eustace Author-email: sebastien@eustace.io License: MIT Download-URL: https://github.com/sdispater/pastel/archive/0.1.0.tar.gz Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/pluggy-0.6.0-py2-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: pluggy Version: 0.6.0 Summary: plugin and hook calling mechanisms for python Home-page: https://github.com/pytest-dev/pluggy Author: Holger Krekel Author-email: holger@merlinux.eu License: MIT license Platform: unix Platform: linux Platform: osx Platform: win32 Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/pluggy-0.6.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: pluggy Version: 0.6.0 Summary: plugin and hook calling mechanisms for python Home-page: https://github.com/pytest-dev/pluggy Author: Holger Krekel Author-email: holger@merlinux.eu License: MIT license Description-Content-Type: UNKNOWN Platform: unix Platform: linux Platform: osx Platform: win32 Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/poetry_core-1.5.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: poetry-core Version: 1.5.0 Summary: Poetry PEP 517 Build Backend Home-page: https://github.com/python-poetry/poetry-core License: MIT Keywords: packaging,dependency,poetry Author: Sébastien Eustace Author-email: sebastien@eustace.io Requires-Python: >=3.7,<4.0 Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Dist: importlib-metadata (>=1.7.0) ; python_version < "3.8" Project-URL: Bug Tracker, https://github.com/python-poetry/poetry/issues Project-URL: Repository, https://github.com/python-poetry/poetry-core Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/poetry_core-2.0.1-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.3 Name: poetry-core Version: 2.0.1 Summary: Poetry PEP 517 Build Backend License: MIT Keywords: packaging,dependency,poetry Author: Sébastien Eustace Author-email: sebastien@eustace.io Maintainer: Arun Babu Neelicattu Maintainer-email: arun.neelicattu@gmail.com Requires-Python: >=3.9, <4.0 Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Project-URL: Bug Tracker, https://github.com/python-poetry/poetry/issues Project-URL: Homepage, https://github.com/python-poetry/poetry-core Project-URL: Repository, https://github.com/python-poetry/poetry-core Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/py-1.5.3-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: py Version: 1.5.3 Summary: library with cross-python path, ini-parsing, io, code, log facilities Home-page: http://py.readthedocs.io/ Author: holger krekel, Ronny Pfannschmidt, Benjamin Peterson and others Author-email: pytest-dev@python.org License: MIT license Platform: unix Platform: linux Platform: osx Platform: cygwin Platform: win32 Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/pylev-1.3.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: pylev Version: 1.3.0 Summary: A pure Python Levenshtein implementation that's not freaking GPL'd. Home-page: http://github.com/toastdriven/pylev Author: Daniel Lindsley Author-email: daniel@toastdriven.com License: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/pytest-3.5.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: pytest Version: 3.5.0 Summary: pytest: simple powerful testing with Python Home-page: http://pytest.org Author: Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others License: MIT license Keywords: test unittest Platform: unix Platform: linux Platform: osx Platform: cygwin Platform: win32 Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Requires-Dist: py (>=1.5.0) Requires-Dist: six (>=1.10.0) Requires-Dist: setuptools Requires-Dist: attrs (>=17.4.0) Requires-Dist: more-itertools (>=4.0.0) Requires-Dist: pluggy (<0.7,>=0.5) Requires-Dist: funcsigs; python_version < "3.0" Requires-Dist: colorama; sys_platform == "win32" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/pytest-3.5.1-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: pytest Version: 3.5.1 Summary: pytest: simple powerful testing with Python Home-page: http://pytest.org Author: Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others License: MIT license Project-URL: Source, https://github.com/pytest-dev/pytest Project-URL: Tracker, https://github.com/pytest-dev/pytest/issues Keywords: test unittest Platform: unix Platform: linux Platform: osx Platform: cygwin Platform: win32 Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Requires-Dist: py (>=1.5.0) Requires-Dist: six (>=1.10.0) Requires-Dist: setuptools Requires-Dist: attrs (>=17.4.0) Requires-Dist: more-itertools (>=4.0.0) Requires-Dist: pluggy (<0.7,>=0.5) Requires-Dist: funcsigs; python_version < "3.0" Requires-Dist: colorama; sys_platform == "win32" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/requests-2.18.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: requests Version: 2.18.0 Summary: Python HTTP for Humans. Home-page: http://python-requests.org Author: Kenneth Reitz Author-email: me@kennethreitz.org License: Apache 2.0 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Dist: certifi (>=2017.4.17) Requires-Dist: chardet (>=3.0.2,<3.1.0) Requires-Dist: idna (>=2.5,<2.6) Requires-Dist: urllib3 (<1.22,>=1.21.1) Provides-Extra: security Requires-Dist: cryptography (>=1.3.4); extra == 'security' Requires-Dist: idna (>=2.0.0); extra == 'security' Requires-Dist: pyOpenSSL (>=0.14); extra == 'security' Provides-Extra: socks Requires-Dist: PySocks (!=1.5.7,>=1.5.6); extra == 'socks' Provides-Extra: socks Requires-Dist: win-inet-pton; sys_platform == "win32" and (python_version == "2.7" or python_version == "2.6") and extra == 'socks' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/requests-2.18.1-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: requests Version: 2.18.1 Summary: Python HTTP for Humans. Home-page: http://python-requests.org Author: Kenneth Reitz Author-email: me@kennethreitz.org License: Apache 2.0 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Dist: certifi (>=2017.4.17) Requires-Dist: chardet (>=3.0.2,<3.1.0) Requires-Dist: idna (>=2.5,<2.6) Requires-Dist: urllib3 (<1.22,>=1.21.1) Provides-Extra: security Requires-Dist: cryptography (>=1.3.4); extra == 'security' Requires-Dist: idna (>=2.0.0); extra == 'security' Requires-Dist: pyOpenSSL (>=0.14); extra == 'security' Provides-Extra: socks Requires-Dist: PySocks (!=1.5.7,>=1.5.6); extra == 'socks' Provides-Extra: socks Requires-Dist: win-inet-pton; sys_platform == "win32" and (python_version == "2.7" or python_version == "2.6") and extra == 'socks' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/requests-2.18.2-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: requests Version: 2.18.2 Summary: Python HTTP for Humans. Home-page: http://python-requests.org Author: Kenneth Reitz Author-email: me@kennethreitz.org License: Apache 2.0 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Dist: certifi (>=2017.4.17) Requires-Dist: chardet (>=3.0.2,<3.1.0) Requires-Dist: idna (>=2.5,<2.6) Requires-Dist: urllib3 (<1.23,>=1.21.1) Provides-Extra: security Requires-Dist: cryptography (>=1.3.4); extra == 'security' Requires-Dist: idna (>=2.0.0); extra == 'security' Requires-Dist: pyOpenSSL (>=0.14); extra == 'security' Provides-Extra: socks Requires-Dist: PySocks (!=1.5.7,>=1.5.6); extra == 'socks' Provides-Extra: socks Requires-Dist: win-inet-pton; sys_platform == "win32" and (python_version == "2.7" or python_version == "2.6") and extra == 'socks' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/requests-2.18.3-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: requests Version: 2.18.3 Summary: Python HTTP for Humans. Home-page: http://python-requests.org Author: Kenneth Reitz Author-email: me@kennethreitz.org License: Apache 2.0 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Dist: certifi (>=2017.4.17) Requires-Dist: chardet (>=3.0.2,<3.1.0) Requires-Dist: idna (>=2.5,<2.6) Requires-Dist: urllib3 (<1.23,>=1.21.1) Provides-Extra: security Requires-Dist: cryptography (>=1.3.4); extra == 'security' Requires-Dist: idna (>=2.0.0); extra == 'security' Requires-Dist: pyOpenSSL (>=0.14); extra == 'security' Provides-Extra: socks Requires-Dist: PySocks (!=1.5.7,>=1.5.6); extra == 'socks' Provides-Extra: socks Requires-Dist: win-inet-pton; sys_platform == "win32" and (python_version == "2.7" or python_version == "2.6") and extra == 'socks' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/requests-2.18.4-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: requests Version: 2.18.4 Summary: Python HTTP for Humans. Home-page: http://python-requests.org Author: Kenneth Reitz Author-email: me@kennethreitz.org License: Apache 2.0 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Dist: certifi (>=2017.4.17) Requires-Dist: chardet (>=3.0.2,<3.1.0) Requires-Dist: idna (>=2.5,<2.7) Requires-Dist: urllib3 (<1.23,>=1.21.1) Provides-Extra: security Requires-Dist: cryptography (>=1.3.4); extra == 'security' Requires-Dist: idna (>=2.0.0); extra == 'security' Requires-Dist: pyOpenSSL (>=0.14); extra == 'security' Provides-Extra: socks Requires-Dist: PySocks (!=1.5.7,>=1.5.6); extra == 'socks' Provides-Extra: socks Requires-Dist: win-inet-pton; sys_platform == "win32" and (python_version == "2.7" or python_version == "2.6") and extra == 'socks' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/requests-2.19.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: requests Version: 2.19.0 Summary: Python HTTP for Humans. Home-page: http://python-requests.org Author: Kenneth Reitz Author-email: me@kennethreitz.org License: Apache 2.0 Description-Content-Type: text/x-rst Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Requires-Dist: chardet (<3.1.0,>=3.0.2) Requires-Dist: idna (<2.8,>=2.5) Requires-Dist: urllib3 (<1.24,>=1.21.1) Requires-Dist: certifi (>=2017.4.17) Provides-Extra: security Requires-Dist: pyOpenSSL (>=0.14); extra == 'security' Requires-Dist: cryptography (>=1.3.4); extra == 'security' Requires-Dist: idna (>=2.0.0); extra == 'security' Provides-Extra: socks Requires-Dist: PySocks (!=1.5.7,>=1.5.6); extra == 'socks' Provides-Extra: socks Requires-Dist: win-inet-pton; sys_platform == "win32" and (python_version == "2.7" or python_version == "2.6") and extra == 'socks' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/setuptools-39.2.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: setuptools Version: 39.2.0 Summary: Easily download, build, install, upgrade, and uninstall Python packages Home-page: https://github.com/pypa/setuptools Author: Python Packaging Authority Author-email: distutils-sig@python.org License: UNKNOWN Project-URL: Documentation, https://setuptools.readthedocs.io/ Keywords: CPAN PyPI distutils eggs package management Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Archiving :: Packaging Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.* Description-Content-Type: text/x-rst; charset=UTF-8 Provides-Extra: ssl Provides-Extra: certs Provides-Extra: certs Requires-Dist: certifi (==2016.9.26); extra == 'certs' Provides-Extra: ssl Requires-Dist: wincertstore (==0.2); (sys_platform=='win32') and extra == 'ssl' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/setuptools-67.6.1-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: setuptools Version: 67.6.1 Summary: Easily download, build, install, upgrade, and uninstall Python packages Home-page: https://github.com/pypa/setuptools Author: Python Packaging Authority Author-email: distutils-sig@python.org Project-URL: Documentation, https://setuptools.pypa.io/ Project-URL: Changelog, https://setuptools.pypa.io/en/stable/history.html Keywords: CPAN PyPI distutils eggs package management Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Archiving :: Packaging Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities Requires-Python: >=3.7 License-File: LICENSE Provides-Extra: certs Provides-Extra: docs Requires-Dist: sphinx (>=3.5) ; extra == 'docs' Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs' Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' Requires-Dist: furo ; extra == 'docs' Requires-Dist: sphinx-lint ; extra == 'docs' Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs' Requires-Dist: pygments-github-lexers (==0.0.5) ; extra == 'docs' Requires-Dist: sphinx-favicon ; extra == 'docs' Requires-Dist: sphinx-inline-tabs ; extra == 'docs' Requires-Dist: sphinx-reredirects ; extra == 'docs' Requires-Dist: sphinxcontrib-towncrier ; extra == 'docs' Requires-Dist: sphinx-notfound-page (==0.8.3) ; extra == 'docs' Requires-Dist: sphinx-hoverxref (<2) ; extra == 'docs' Provides-Extra: ssl Provides-Extra: testing Requires-Dist: pytest (>=6) ; extra == 'testing' Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' Requires-Dist: flake8 (<5) ; extra == 'testing' Requires-Dist: pytest-enabler (>=1.3) ; extra == 'testing' Requires-Dist: pytest-perf ; extra == 'testing' Requires-Dist: flake8-2020 ; extra == 'testing' Requires-Dist: virtualenv (>=13.0.0) ; extra == 'testing' Requires-Dist: wheel ; extra == 'testing' Requires-Dist: pip (>=19.1) ; extra == 'testing' Requires-Dist: jaraco.envs (>=2.2) ; extra == 'testing' Requires-Dist: pytest-xdist ; extra == 'testing' Requires-Dist: jaraco.path (>=3.2.0) ; extra == 'testing' Requires-Dist: build[virtualenv] ; extra == 'testing' Requires-Dist: filelock (>=3.4.0) ; extra == 'testing' Requires-Dist: pip-run (>=8.8) ; extra == 'testing' Requires-Dist: ini2toml[lite] (>=0.9) ; extra == 'testing' Requires-Dist: tomli-w (>=1.0.0) ; extra == 'testing' Requires-Dist: pytest-timeout ; extra == 'testing' Provides-Extra: testing-integration Requires-Dist: pytest ; extra == 'testing-integration' Requires-Dist: pytest-xdist ; extra == 'testing-integration' Requires-Dist: pytest-enabler ; extra == 'testing-integration' Requires-Dist: virtualenv (>=13.0.0) ; extra == 'testing-integration' Requires-Dist: tomli ; extra == 'testing-integration' Requires-Dist: wheel ; extra == 'testing-integration' Requires-Dist: jaraco.path (>=3.2.0) ; extra == 'testing-integration' Requires-Dist: jaraco.envs (>=2.2) ; extra == 'testing-integration' Requires-Dist: build[virtualenv] ; extra == 'testing-integration' Requires-Dist: filelock (>=3.4.0) ; extra == 'testing-integration' Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' Requires-Dist: pytest-cov ; (platform_python_implementation != "PyPy") and extra == 'testing' Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing' Requires-Dist: pytest-flake8 ; (python_version < "3.12") and extra == 'testing' ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/six-1.11.0-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: six Version: 1.11.0 Summary: Python 2 and 3 compatibility utilities Home-page: http://pypi.python.org/pypi/six/ Author: Benjamin Peterson Author-email: benjamin@python.org License: MIT Platform: UNKNOWN Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/toga-0.3.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: toga Version: 0.3.0 Summary: A Python native, OS native GUI toolkit. Home-page: https://beeware.org/project/projects/libraries/toga/ Author: Russell Keith-Magee Author-email: russell@keith-magee.com Maintainer: BeeWare Team Maintainer-email: team@beeware.org License: New BSD Project-URL: Funding, https://beeware.org/contributing/membership/ Project-URL: Documentation, http://toga.readthedocs.io/en/latest/ Project-URL: Tracker, https://github.com/beeware/toga/issues Project-URL: Source, https://github.com/beeware/toga Keywords: gui,widget,cross-platform,desktop,mobile,web,macOS,cocoa,iOS,android,windows,winforms,linux,gtk Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: Software Development :: Widget Sets Requires-Python: >=3.7 Description-Content-Type: text/x-rst; charset=UTF-8 Requires-Dist: toga-cocoa (==0.3.0) ; sys_platform=="darwin" Requires-Dist: toga-gtk (==0.3.0) ; sys_platform=="linux" Requires-Dist: toga-winforms (==0.3.0) ; sys_platform=="win32" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/toga-0.3.0.dev1-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: toga Version: 0.3.0.dev1 Summary: A Python native, OS native GUI toolkit. Home-page: http://pybee.org/toga Author: Russell Keith-Magee Author-email: russell@keith-magee.com License: New BSD Description-Content-Type: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: Software Development :: Widget Sets Requires-Dist: toga-cocoa; sys_platform=="darwin" Requires-Dist: toga-gtk; sys_platform=="linux" Requires-Dist: toga-winforms; sys_platform=="win32" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/toga-0.3.0.dev2-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.0 Name: toga Version: 0.3.0.dev2 Summary: A Python native, OS native GUI toolkit. Home-page: http://pybee.org/toga Author: Russell Keith-Magee Author-email: russell@keith-magee.com License: New BSD Description-Content-Type: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: Software Development :: Widget Sets Requires-Dist: toga-cocoa; sys_platform=="darwin" Requires-Dist: toga-gtk; sys_platform=="linux" Requires-Dist: toga-winforms; sys_platform=="win32" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/toga-0.4.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: toga Version: 0.4.0 Summary: A Python native, OS native GUI toolkit. Home-page: https://beeware.org/project/projects/libraries/toga/ Author: Russell Keith-Magee Author-email: russell@keith-magee.com Maintainer: BeeWare Team Maintainer-email: team@beeware.org License: New BSD Project-URL: Funding, https://beeware.org/contributing/membership/ Project-URL: Documentation, http://toga.readthedocs.io/en/latest/ Project-URL: Tracker, https://github.com/beeware/toga/issues Project-URL: Source, https://github.com/beeware/toga Keywords: gui,widget,cross-platform,desktop,mobile,web,macOS,cocoa,iOS,android,windows,winforms,linux,freeBSD,gtk Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: Software Development :: Widget Sets Requires-Python: >=3.8 Description-Content-Type: text/x-rst; charset=UTF-8 Requires-Dist: toga-gtk ==0.4.0 ; "freebsd" in sys_platform Requires-Dist: toga-cocoa ==0.4.0 ; sys_platform=="darwin" Requires-Dist: toga-gtk ==0.4.0 ; sys_platform=="linux" Requires-Dist: toga-winforms ==0.4.0 ; sys_platform=="win32" ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/tomlkit-0.5.2-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: tomlkit Version: 0.5.2 Summary: Style preserving TOML library Home-page: https://github.com/sdispater/tomlkit License: MIT Author: Sébastien Eustace Author-email: sebastien@eustace.io Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Requires-Dist: enum34 (>=1.1,<2.0); python_version >= "2.7" and python_version < "2.8" Requires-Dist: functools32 (>=3.2.3,<4.0.0); python_version >= "2.7" and python_version < "2.8" Requires-Dist: typing (>=3.6,<4.0); python_version >= "2.7" and python_version < "2.8" or python_version >= "3.4" and python_version < "3.5" Project-URL: Repository, https://github.com/sdispater/tomlkit Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/tomlkit-0.5.3-py2.py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: tomlkit Version: 0.5.3 Summary: Style preserving TOML library Home-page: https://github.com/sdispater/tomlkit License: MIT Author: Sébastien Eustace Author-email: sebastien@eustace.io Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Requires-Dist: enum34 (>=1.1,<2.0); python_version >= "2.7" and python_version < "2.8" Requires-Dist: functools32 (>=3.2.3,<4.0.0); python_version >= "2.7" and python_version < "2.8" Requires-Dist: typing (>=3.6,<4.0); python_version >= "2.7" and python_version < "2.8" or python_version >= "3.4" and python_version < "3.5" Project-URL: Repository, https://github.com/sdispater/tomlkit Description-Content-Type: text/markdown ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/wheel-0.40.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: wheel Version: 0.40.0 Summary: A built-package format for Python Keywords: wheel,packaging Author-email: Daniel Holth Maintainer-email: Alex Grönholm Requires-Python: >=3.7 Description-Content-Type: text/x-rst Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: System :: Archiving :: Packaging Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Requires-Dist: pytest >= 6.0.0 ; extra == "test" Project-URL: Changelog, https://wheel.readthedocs.io/en/stable/news.html Project-URL: Documentation, https://wheel.readthedocs.io/ Project-URL: Issue Tracker, https://github.com/pypa/wheel/issues Provides-Extra: test ================================================ FILE: tests/repositories/fixtures/pypi.org/metadata/zipp-3.5.0-py3-none-any.whl.metadata ================================================ Metadata-Version: 2.1 Name: zipp Version: 3.5.0 Summary: Backport of pathlib-compatible object wrapper for zip files Home-page: https://github.com/jaraco/zipp Author: Jason R. Coombs Author-email: jaraco@jaraco.com License: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Requires-Python: >=3.6 License-File: LICENSE Provides-Extra: docs Requires-Dist: sphinx ; extra == 'docs' Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' Provides-Extra: testing Requires-Dist: pytest (>=4.6) ; extra == 'testing' Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' Requires-Dist: pytest-flake8 ; extra == 'testing' Requires-Dist: pytest-cov ; extra == 'testing' Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' Requires-Dist: jaraco.itertools ; extra == 'testing' Requires-Dist: func-timeout ; extra == 'testing' Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy" and python_version < "3.10") and extra == 'testing' Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy" and python_version < "3.10") and extra == 'testing' ================================================ FILE: tests/repositories/fixtures/pypi.org/search/search-disallowed.html ================================================
A required part of this site couldn’t load. This may be due to a browser extension, network issues, or browser settings. Please check your connection, disable any ad blockers, or try using a different browser.
================================================ FILE: tests/repositories/fixtures/pypi.org/search/search.html ================================================ Search results · PyPI
Skip to main content

Filter by classifier

Search results

1,847 projects for "sqlalchemy"

Previous 1 2 3 ... 93 Next
================================================ FILE: tests/repositories/fixtures/pypi.py ================================================ from __future__ import annotations import json import re from typing import TYPE_CHECKING from typing import Any from urllib.parse import urlparse import pytest import responses from packaging.utils import parse_sdist_filename from packaging.utils import parse_wheel_filename from poetry.repositories.pypi_repository import PyPiRepository from tests.helpers import FIXTURE_PATH_DISTRIBUTIONS from tests.helpers import FIXTURE_PATH_REPOSITORIES_PYPI if TYPE_CHECKING: from collections.abc import Callable from pathlib import Path from requests import PreparedRequest from tests.types import HttpRequestCallback from tests.types import HttpResponse from tests.types import PackageDistributionLookup pytest_plugins = [ "tests.repositories.fixtures.legacy", "tests.repositories.fixtures.python_hosted", ] @pytest.fixture def package_distribution_locations() -> list[Path]: return [ FIXTURE_PATH_REPOSITORIES_PYPI / "dists", FIXTURE_PATH_REPOSITORIES_PYPI / "dists" / "mocked", FIXTURE_PATH_REPOSITORIES_PYPI / "stubbed", FIXTURE_PATH_DISTRIBUTIONS, ] @pytest.fixture def package_json_locations() -> list[Path]: return [ FIXTURE_PATH_REPOSITORIES_PYPI / "json", FIXTURE_PATH_REPOSITORIES_PYPI / "json" / "mocked", ] @pytest.fixture def package_metadata_locations() -> list[Path]: return [ FIXTURE_PATH_REPOSITORIES_PYPI / "metadata", FIXTURE_PATH_REPOSITORIES_PYPI / "metadata" / "mocked", ] @pytest.fixture def package_distribution_lookup( package_distribution_locations: list[Path], ) -> PackageDistributionLookup: def lookup(name: str) -> Path | None: for location in package_distribution_locations: fixture = location / name if fixture.exists(): return fixture return None return lookup @pytest.fixture def with_disallowed_pypi_search_html( http: responses.RequestsMock, pypi_repository: PyPiRepository ) -> None: def search_callback(request: PreparedRequest) -> HttpResponse: search_html = FIXTURE_PATH_REPOSITORIES_PYPI.joinpath( "search", "search-disallowed.html" ) return 200, {}, search_html.read_bytes() search_url_regex = re.compile(r"https://pypi\.org/search(\?(.*))?$") http.remove(responses.GET, search_url_regex) http.add_callback( responses.GET, search_url_regex, callback=search_callback, ) @pytest.fixture(autouse=True) def pypi_repository( http: responses.RequestsMock, legacy_repository_html_callback: HttpRequestCallback, package_json_locations: list[Path], mock_files_python_hosted: None, ) -> PyPiRepository: def default_callback(request: PreparedRequest) -> HttpResponse: return 404, {}, b"Not Found" def search_callback(request: PreparedRequest) -> HttpResponse: search_html = FIXTURE_PATH_REPOSITORIES_PYPI.joinpath("search", "search.html") return 200, {}, search_html.read_bytes() def simple_callback(request: PreparedRequest) -> HttpResponse: if request.headers.get("Accept") == "application/vnd.pypi.simple.v1+json": return json_callback(request) return legacy_repository_html_callback(request) def _get_json_filepath(name: str, version: str | None = None) -> Path | None: for base in package_json_locations: if not version: fixture = base / f"{name}.json" else: fixture = base / name / f"{version}.json" if fixture.exists(): return fixture return None def json_callback(request: PreparedRequest) -> HttpResponse: assert request.url path = urlparse(request.url).path parts = path.rstrip("/").split("/")[2:] name = parts[0] version = parts[1] if len(parts) == 3 else None fixture = _get_json_filepath(name, version) if fixture is None or not fixture.exists(): return default_callback(request) return 200, {}, fixture.read_bytes() http.add_callback( responses.GET, re.compile(r"https://pypi\.org/search(\?(.*))?$"), callback=search_callback, ) http.add_callback( responses.GET, re.compile(r"https://pypi\.org/pypi/(.*)?/json$"), callback=json_callback, ) http.add_callback( responses.GET, re.compile(r"https://pypi\.org/pypi/(?!.*?/json$)(.*)$"), callback=default_callback, ) http.add_callback( responses.GET, re.compile(r"https://pypi\.org/simple/?(.*)?$"), callback=simple_callback, ) return PyPiRepository(disable_cache=True, fallback=False) @pytest.fixture def get_pypi_file_info( package_json_locations: list[Path], ) -> Callable[[str], dict[str, Any]]: def get_file_info(name: str) -> dict[str, Any]: if name.endswith(".whl"): package_name, version, _build, _tags = parse_wheel_filename(name) else: package_name, version = parse_sdist_filename(name) path = package_json_locations[0] / package_name if not path.exists(): raise RuntimeError( f"Fixture for {package_name} not found in pypi.org json fixtures" ) path /= f"{version}.json" if not path.exists(): raise RuntimeError( f"Fixture for {package_name} {version} not found in pypi.org json fixtures" ) with path.open("rb") as f: content = json.load(f) for url in content["urls"]: if url["filename"] == name: return url # type: ignore[no-any-return] raise RuntimeError(f"No URL in pypi.org json fixture of {name} {version}") return get_file_info ================================================ FILE: tests/repositories/fixtures/python_hosted.py ================================================ from __future__ import annotations import re from collections.abc import Iterator from pathlib import Path from typing import TYPE_CHECKING from urllib.parse import urlparse import pytest import responses if TYPE_CHECKING: from collections.abc import Iterator from requests import PreparedRequest from tests.types import HttpResponse from tests.types import PythonHostedFileMocker @pytest.fixture def mock_files_python_hosted_factory( http: responses.RequestsMock, ) -> PythonHostedFileMocker: def factory( distribution_locations: list[Path], metadata_locations: list[Path] ) -> None: def file_callback(request: PreparedRequest) -> HttpResponse: assert request.url name = Path(urlparse(request.url).path).name locations = ( metadata_locations if name.endswith(".metadata") else distribution_locations ) for location in locations: fixture = location / name if fixture.exists(): return 200, {}, fixture.read_bytes() return 404, {}, b"Not Found" def mock_file_callback(request: PreparedRequest) -> HttpResponse: return 200, {}, b"" http.add_callback( responses.GET, re.compile(r"^https://files\.pythonhosted\.org/.*$"), callback=file_callback, ) http.add_callback( responses.GET, re.compile(r"^https://mock\.pythonhosted\.org/.*$"), callback=mock_file_callback, ) return factory @pytest.fixture def mock_files_python_hosted( mock_files_python_hosted_factory: PythonHostedFileMocker, package_distribution_locations: list[Path], package_metadata_locations: list[Path], ) -> Iterator[None]: mock_files_python_hosted_factory( distribution_locations=package_distribution_locations, metadata_locations=package_metadata_locations, ) yield None ================================================ FILE: tests/repositories/fixtures/single-page/jax_releases.html ================================================ nocuda/jaxlib-0.3.0-cp310-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.0-cp37-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.0-cp38-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.0-cp39-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.2-cp310-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.2-cp37-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.2-cp38-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.2-cp39-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.5-cp310-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.5-cp37-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.5-cp38-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.5-cp39-none-manylinux2010_x86_64.whl
nocuda/jaxlib-0.3.7-cp310-none-manylinux2014_x86_64.whl
nocuda/jaxlib-0.3.7-cp37-none-manylinux2014_x86_64.whl
nocuda/jaxlib-0.3.7-cp38-none-manylinux2014_x86_64.whl
nocuda/jaxlib-0.3.7-cp39-none-manylinux2014_x86_64.whl
jax/jax-0.3.0.tar.gz
jax/jax-0.3.2.tar.gz
jax/jax-0.3.5.tar.gz
jax/jax-0.3.6.tar.gz
jax/jax-0.3.7.tar.gz
================================================ FILE: tests/repositories/fixtures/single-page/mmcv_torch_releases.html ================================================ ../torch1.12.0/mmcv-2.0.0-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv-2.0.0-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv-2.0.0-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv-2.0.0-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc1-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc1-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc1-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc1-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc1-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc1-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc1-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc1-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc2-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc2-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc2-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc2-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc2-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc2-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc2-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc2-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc3-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc3-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc3-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc3-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc3-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc3-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc3-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc3-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc4-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc4-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc4-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc4-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc4-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc4-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv-2.0.0rc4-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.0rc4-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv-2.0.1-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.1-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv-2.0.1-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.1-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv-2.0.1-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.1-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv-2.0.1-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.0.1-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv-2.1.0-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.1.0-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv-2.1.0-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.1.0-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv-2.1.0-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.1.0-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv-2.1.0-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv-2.1.0-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.0-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.0-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.0-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.0-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.0-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.0-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.0-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.0-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.1-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.1-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.1-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.1-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.1-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.1-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.1-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.1-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.2-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.2-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.2-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.2-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.2-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.2-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv_full-1.6.2-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.6.2-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.0-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.0-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.0-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.0-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.0-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.0-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.0-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.0-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.1-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.1-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.1-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.1-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.1-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.1-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.1-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.1-cp39-cp39-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.2-cp310-cp310-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.2-cp310-cp310-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.2-cp37-cp37m-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.2-cp37-cp37m-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.2-cp38-cp38-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.2-cp38-cp38-win_amd64.whl
../torch1.12.0/mmcv_full-1.7.2-cp39-cp39-manylinux1_x86_64.whl
../torch1.12.0/mmcv_full-1.7.2-cp39-cp39-win_amd64.whl
================================================ FILE: tests/repositories/link_sources/__init__.py ================================================ ================================================ FILE: tests/repositories/link_sources/test_base.py ================================================ from __future__ import annotations from collections import defaultdict from functools import cached_property from typing import TYPE_CHECKING from unittest.mock import PropertyMock import pytest from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link from poetry.repositories.link_sources.base import LinkSource from poetry.repositories.link_sources.base import SimpleRepositoryRootPage if TYPE_CHECKING: from collections.abc import Iterable from pytest_mock import MockerFixture @pytest.fixture def root_page() -> SimpleRepositoryRootPage: class TestRootPage(SimpleRepositoryRootPage): @cached_property def package_names(self) -> list[str]: return ["poetry", "poetry-core", "requests", "urllib3"] return TestRootPage() @pytest.fixture def link_source(mocker: MockerFixture) -> LinkSource: url = "https://example.org" link_source = LinkSource(url) mocker.patch( f"{LinkSource.__module__}.{LinkSource.__qualname__}._link_cache", new_callable=PropertyMock, return_value=defaultdict( lambda: defaultdict(list), { canonicalize_name("demo"): defaultdict( list, { Version.parse("0.1.0"): [ Link(f"{url}/demo-0.1.0.tar.gz"), Link(f"{url}/demo-0.1.0-py2.py3-none-any.whl"), ], Version.parse("0.1.1"): [Link(f"{url}/demo-0.1.1.tar.gz")], }, ), }, ), ) return link_source @pytest.mark.parametrize( "filename, expected", [ ("demo-0.1.0-py2.py3-none-any.whl", Package("demo", "0.1.0")), ("demo-0.1.0.tar.gz", Package("demo", "0.1.0")), ("demo-0.1.0.egg", Package("demo", "0.1.0")), ("demo-0.1.0_invalid-py2.py3-none-any.whl", None), # invalid version ("demo-0.1.0_invalid.egg", None), # invalid version ("no-package-at-all.txt", None), ], ) def test_link_package_data(filename: str, expected: Package | None) -> None: link = Link(f"https://example.org/{filename}") assert LinkSource.link_package_data(link) == expected @pytest.mark.parametrize( "name, expected", [ ("demo", {Version.parse("0.1.0"), Version.parse("0.1.1")}), ("invalid", set()), ], ) def test_versions(name: str, expected: set[Version], link_source: LinkSource) -> None: assert set(link_source.versions(canonicalize_name(name))) == expected def test_packages(link_source: LinkSource) -> None: expected = { Package("demo", "0.1.0"), Package("demo", "0.1.0"), Package("demo", "0.1.1"), } assert set(link_source.packages) == expected @pytest.mark.parametrize( "version_string, filenames", [ ("0.1.0", ["demo-0.1.0.tar.gz", "demo-0.1.0-py2.py3-none-any.whl"]), ("0.1.1", ["demo-0.1.1.tar.gz"]), ("0.1.2", []), ], ) def test_links_for_version( version_string: str, filenames: Iterable[str], link_source: LinkSource ) -> None: version = Version.parse(version_string) expected = {Link(f"{link_source.url}/{name}") for name in filenames} assert ( set(link_source.links_for_version(canonicalize_name("demo"), version)) == expected ) @pytest.mark.parametrize( "query, expected", [ ("poetry", ["poetry", "poetry-core"]), (["requests", "urllib3"], ["requests", "urllib3"]), ("lib", ["urllib3"]), ("nonexistent", []), ], ) def test_root_page_search( root_page: SimpleRepositoryRootPage, query: str | list[str], expected: list[str] ) -> None: assert root_page.search(query) == expected ================================================ FILE: tests/repositories/link_sources/test_html.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.packages.utils.link import Link from poetry.repositories.link_sources.html import HTMLPage from poetry.repositories.link_sources.html import SimpleRepositoryHTMLRootPage if TYPE_CHECKING: from tests.types import HTMLPageGetter @pytest.fixture def root_page() -> SimpleRepositoryHTMLRootPage: names = ["poetry", "poetry-core", "requests"] hrefs = [f'{name}
' for name in names] return SimpleRepositoryHTMLRootPage(f"""\ Legacy Repository {"".join(hrefs)} """) def test_root_page_package_names(root_page: SimpleRepositoryHTMLRootPage) -> None: assert root_page.package_names == ["poetry", "poetry-core", "requests"] @pytest.mark.parametrize( "attributes, expected_link", [ ("", Link("https://example.org/demo-0.1.whl")), ( 'data-requires-python=">=3.7"', Link("https://example.org/demo-0.1.whl", requires_python=">=3.7"), ), ( "data-yanked", Link("https://example.org/demo-0.1.whl", yanked=True), ), ( 'data-yanked=""', Link("https://example.org/demo-0.1.whl", yanked=True), ), ( 'data-yanked="<reason>"', Link("https://example.org/demo-0.1.whl", yanked=""), ), ( 'data-requires-python=">=3.7" data-yanked', Link( "https://example.org/demo-0.1.whl", requires_python=">=3.7", yanked=True ), ), ], ) def test_link_attributes( html_page_content: HTMLPageGetter, attributes: str, expected_link: Link ) -> None: anchor = ( f'demo-0.1.whl
' ) content = html_page_content(anchor) page = HTMLPage("https://example.org", content) assert len(list(page.links)) == 1 link = next(iter(page.links)) assert link.url == expected_link.url assert link.requires_python == expected_link.requires_python assert link.yanked == expected_link.yanked assert link.yanked_reason == expected_link.yanked_reason def test_hash_from_url(html_page_content: HTMLPageGetter) -> None: anchor = ( 'demo-1.0.0.whl
' ) content = html_page_content(anchor) page = HTMLPage("https://example.org", content) assert len(list(page.links)) == 1 link = next(iter(page.links)) assert link.hashes == {"sha256": "abcd1234"} @pytest.mark.parametrize( "yanked_attrs, expected", [ (("", ""), False), (("data-yanked", ""), False), (("", "data-yanked"), False), (("data-yanked", "data-yanked"), True), (("data-yanked='reason'", "data-yanked"), "reason"), (("data-yanked", "data-yanked='reason'"), "reason"), (("data-yanked='reason'", "data-yanked=''"), "reason"), (("data-yanked=''", "data-yanked='reason'"), "reason"), (("data-yanked='reason'", "data-yanked='reason'"), "reason"), (("data-yanked='reason 1'", "data-yanked='reason 2'"), "reason 1\nreason 2"), ], ) def test_yanked( html_page_content: HTMLPageGetter, yanked_attrs: tuple[str, str], expected: bool | str, ) -> None: anchors = ( f'' "demo-0.1.tar.gz" f'demo-0.1.whl' ) content = html_page_content(anchors) page = HTMLPage("https://example.org", content) assert page.yanked(canonicalize_name("demo"), Version.parse("0.1")) == expected @pytest.mark.parametrize( ("metadata", "expected_has_metadata", "expected_metadata_hashes"), [ ("", False, {}), # new ("data-core-metadata", True, {}), ("data-core-metadata=''", True, {}), ("data-core-metadata='foo'", True, {}), ("data-core-metadata='sha256=abcd'", True, {"sha256": "abcd"}), # old ("data-dist-info-metadata", True, {}), ("data-dist-info-metadata=''", True, {}), ("data-dist-info-metadata='foo'", True, {}), ("data-dist-info-metadata='sha256=abcd'", True, {"sha256": "abcd"}), # conflicting (new wins) ("data-core-metadata data-dist-info-metadata='sha256=abcd'", True, {}), ("data-dist-info-metadata='sha256=abcd' data-core-metadata", True, {}), ( "data-core-metadata='sha256=abcd' data-dist-info-metadata", True, {"sha256": "abcd"}, ), ( "data-dist-info-metadata data-core-metadata='sha256=abcd'", True, {"sha256": "abcd"}, ), ( "data-core-metadata='sha256=abcd' data-dist-info-metadata='sha256=1234'", True, {"sha256": "abcd"}, ), ( "data-dist-info-metadata='sha256=1234' data-core-metadata='sha256=abcd'", True, {"sha256": "abcd"}, ), ], ) def test_metadata( html_page_content: HTMLPageGetter, metadata: str, expected_has_metadata: bool, expected_metadata_hashes: dict[str, str], ) -> None: anchors = f'demo-0.1.whl' content = html_page_content(anchors) page = HTMLPage("https://example.org", content) link = next(page.links) assert link.has_metadata is expected_has_metadata assert link.metadata_hashes == expected_metadata_hashes @pytest.mark.parametrize( "anchor, base_url, repo_url, expected", ( ( 'demo-0.1.whl', None, "https://example.org/simple/", "https://example.org/demo-0.1.whl", ), ( 'demo-0.1.whl', "https://example.org/files/", "https://example.org/simple/", "https://example.org/demo-0.1.whl", ), ( 'demo-0.1.whl', "https://example.org/files/", "https://example.org/simple/", "https://example.org/files/demo-0.1.whl", ), ( 'demo-0.1.whl', None, "https://example.org/simple/", "https://example.org/simple/demo-0.1.whl", ), ), ) def test_base_url( html_page_content: HTMLPageGetter, anchor: str, base_url: str | None, repo_url: str, expected: str, ) -> None: content = html_page_content(anchor, base_url) page = HTMLPage(repo_url, content) link = next(iter(page.links)) assert link.url == expected ================================================ FILE: tests/repositories/link_sources/test_json.py ================================================ from __future__ import annotations import pytest from packaging.utils import canonicalize_name from poetry.core.constraints.version.version import Version from poetry.repositories.link_sources.json import SimpleJsonPage from poetry.repositories.link_sources.json import SimpleRepositoryJsonRootPage @pytest.fixture def root_page() -> SimpleRepositoryJsonRootPage: names = ["poetry", "poetry-core", "requests"] return SimpleRepositoryJsonRootPage( { "meta": {"api-version": "1.4"}, "projects": [{"name": name} for name in names], } ) def test_root_page_package_names(root_page: SimpleRepositoryJsonRootPage) -> None: assert root_page.package_names == ["poetry", "poetry-core", "requests"] def test_attributes() -> None: content = { "files": [ # minimal {"url": "https://example.org/demo-0.1.whl"}, # all (with non-default values) { "url": "https://example.org/demo-0.1.tar.gz", "requires-python": ">=3.6", "yanked": True, "hashes": {"sha256": "abcd1234"}, "core-metadata": True, }, ] } page = SimpleJsonPage("https://example.org", content) assert page.url == "https://example.org" links = list(page.links) assert len(links) == 2 assert links[0].url == "https://example.org/demo-0.1.whl" assert links[0].requires_python is None assert links[0].yanked is False assert links[0].hashes == {} assert links[0].has_metadata is False assert links[1].url == "https://example.org/demo-0.1.tar.gz" assert links[1].requires_python == ">=3.6" assert links[1].yanked is True assert links[1].hashes == {"sha256": "abcd1234"} assert links[1].has_metadata is True @pytest.mark.parametrize( ("yanked", "expected"), [ ((None, None), False), ((False, False), False), ((True, False), False), ((False, True), False), ((True, True), True), (("reason", True), "reason"), ((True, "reason"), "reason"), (("reason", "reason"), "reason"), (("reason 1", "reason 2"), "reason 1\nreason 2"), ], ) def test_yanked( yanked: tuple[str | None, str | None], expected: bool | str, ) -> None: content = { "files": [ {"url": "https://example.org/demo-0.1.tar.gz", "yanked": yanked[0]}, {"url": "https://example.org/demo-0.1.whl", "yanked": yanked[1]}, ] } if yanked[0] is None: del content["files"][0]["yanked"] if yanked[1] is None: del content["files"][1]["yanked"] page = SimpleJsonPage("https://example.org", content) assert page.yanked(canonicalize_name("demo"), Version.parse("0.1")) == expected @pytest.mark.parametrize( ("metadata", "expected_has_metadata", "expected_metadata_hashes"), [ ({}, False, {}), # new ({"core-metadata": False}, False, {}), ({"core-metadata": True}, True, {}), ( {"core-metadata": {"sha1": "1234", "sha256": "abcd"}}, True, {"sha1": "1234", "sha256": "abcd"}, ), ({"core-metadata": {}}, False, {}), ( {"core-metadata": {"sha1": "1234", "sha256": "abcd"}}, True, {"sha1": "1234", "sha256": "abcd"}, ), # old ({"dist-info-metadata": False}, False, {}), ({"dist-info-metadata": True}, True, {}), ({"dist-info-metadata": {"sha256": "abcd"}}, True, {"sha256": "abcd"}), ({"dist-info-metadata": {}}, False, {}), ( {"dist-info-metadata": {"sha1": "1234", "sha256": "abcd"}}, True, {"sha1": "1234", "sha256": "abcd"}, ), # conflicting (new wins) ({"core-metadata": False, "dist-info-metadata": True}, False, {}), ( {"core-metadata": False, "dist-info-metadata": {"sha256": "abcd"}}, False, {}, ), ({"core-metadata": True, "dist-info-metadata": False}, True, {}), ( {"core-metadata": True, "dist-info-metadata": {"sha256": "abcd"}}, True, {}, ), ( {"core-metadata": {"sha256": "abcd"}, "dist-info-metadata": False}, True, {"sha256": "abcd"}, ), ( {"core-metadata": {"sha256": "abcd"}, "dist-info-metadata": True}, True, {"sha256": "abcd"}, ), ( { "core-metadata": {"sha256": "abcd"}, "dist-info-metadata": {"sha256": "1234"}, }, True, {"sha256": "abcd"}, ), ], ) def test_metadata( metadata: dict[str, bool | dict[str, str]], expected_has_metadata: bool, expected_metadata_hashes: dict[str, str], ) -> None: content = {"files": [{"url": "https://example.org/demo-0.1.whl", **metadata}]} page = SimpleJsonPage("https://example.org", content) link = next(page.links) assert link.has_metadata is expected_has_metadata assert link.metadata_hashes == expected_metadata_hashes @pytest.mark.parametrize( ("url", "repo_url", "expected"), ( ( "https://example.org/files/demo-0.1.whl", "https://example.org/simple/", "https://example.org/files/demo-0.1.whl", ), ( "demo-0.1.whl", "https://example.org/simple/", "https://example.org/simple/demo-0.1.whl", ), ), ) def test_base_url(url: str, repo_url: str, expected: str) -> None: page = SimpleJsonPage(repo_url, {"files": [{"url": url}]}) link = next(iter(page.links)) assert link.url == expected ================================================ FILE: tests/repositories/parsers/__init__.py ================================================ ================================================ FILE: tests/repositories/parsers/test_html_page_parser.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.repositories.parsers.html_page_parser import HTMLPageParser if TYPE_CHECKING: from tests.types import HTMLPageGetter @pytest.fixture() def html_page(html_page_content: HTMLPageGetter) -> str: links = """ demo-0.1.whl
demo-0.1.whl
demo-0.1.whl
demo-0.1.whl
demo-0.1.whl
demo-0.1.whl
""" return html_page_content(links) def test_html_page_parser_anchors(html_page: str) -> None: parser = HTMLPageParser() parser.feed(html_page) assert parser.anchors == [ {"href": "https://example.org/demo-0.1.whl"}, {"data-requires-python": ">=3.7", "href": "https://example.org/demo-0.1.whl"}, {"data-yanked": None, "href": "https://example.org/demo-0.1.whl"}, {"data-yanked": "", "href": "https://example.org/demo-0.1.whl"}, {"data-yanked": "", "href": "https://example.org/demo-0.1.whl"}, { "data-requires-python": ">=3.7", "data-yanked": None, "href": "https://example.org/demo-0.1.whl", }, ] def test_html_page_parser_base_url() -> None: content = """ Links for demo

Links for demo

demo-0.1.whl
""" parser = HTMLPageParser() parser.feed(content) assert parser.base_url == "https://example.org/" ================================================ FILE: tests/repositories/parsers/test_pypi_search_parser.py ================================================ from __future__ import annotations from pathlib import Path import pytest from poetry.repositories.parsers.pypi_search_parser import Result from poetry.repositories.parsers.pypi_search_parser import SearchResultParser FIXTURES_DIRECTORY = Path(__file__).parent.parent / "fixtures" / "pypi.org" / "search" @pytest.fixture def search_page_data() -> str: with FIXTURES_DIRECTORY.joinpath("search.html").open(encoding="utf-8") as f: return f.read() def test_search_parser(search_page_data: str) -> None: parser = SearchResultParser() parser.feed(search_page_data) assert parser.results == [ Result( name="SQLAlchemy", version="1.3.10", description="Database Abstraction Library", ), Result( name="SQLAlchemy-Dao", version="1.3.1", description="Simple wrapper for sqlalchemy.", ), Result( name="graphene-sqlalchemy", version="2.2.2", description="Graphene SQLAlchemy integration", ), Result( name="SQLAlchemy-UTCDateTime", version="1.0.4", description=( "Convert to/from timezone aware datetimes when storing in a DBMS" ), ), Result( name="paginate_sqlalchemy", version="0.3.0", description="Extension to paginate.Page that supports SQLAlchemy queries", ), Result( name="sqlalchemy_audit", version="0.1.0", description=( "sqlalchemy-audit provides an easy way to set up revision " "tracking for your data." ), ), Result( name="transmogrify.sqlalchemy", version="1.0.2", description="Feed data from SQLAlchemy into a transmogrifier pipeline", ), Result( name="sqlalchemy_schemadisplay", version="1.3", description="Turn SQLAlchemy DB Model into a graph", ), Result(name="sqlalchemy_traversal", version="0.5.2", description="UNKNOWN"), Result( name="sqlalchemy-filters", version="0.10.0", description="A library to filter SQLAlchemy queries.", ), Result( name="SQLAlchemy-wrap", version="2.1.7", description="Python wrapper for the CircleCI API", ), Result( name="sqlalchemy-nav", version="0.0.2", description=( "SQLAlchemy-Nav provides SQLAlchemy Mixins for creating " "navigation bars compatible with Bootstrap" ), ), Result( name="sqlalchemy-repr", version="0.0.1", description="Automatically generates pretty repr of a SQLAlchemy model.", ), Result( name="sqlalchemy-diff", version="0.1.3", description="Compare two database schemas using sqlalchemy.", ), Result( name="SQLAlchemy-Equivalence", version="0.1.1", description=( "Provides natural equivalence support for SQLAlchemy " "declarative models." ), ), Result( name="Broadway-SQLAlchemy", version="0.0.1", description="A broadway extension wrapping Flask-SQLAlchemy", ), Result( name="jsonql-sqlalchemy", version="1.0.1", description="Simple JSON-Based CRUD Query Language for SQLAlchemy", ), Result( name="sqlalchemy-plus", version="0.2.0", description="Create Views and Materialized Views with SqlAlchemy", ), Result( name="CherryPy-SQLAlchemy", version="0.5.3", description="Use SQLAlchemy with CherryPy", ), Result( name="sqlalchemy_sqlany", version="1.0.3", description="SAP Sybase SQL Anywhere dialect for SQLAlchemy", ), ] ================================================ FILE: tests/repositories/test_cached_repository.py ================================================ from __future__ import annotations from typing import Any import pytest from packaging.utils import NormalizedName from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.inspection.info import PackageInfo from poetry.repositories.cached_repository import CachedRepository class MockCachedRepository(CachedRepository): def _get_release_info( self, name: NormalizedName, version: Version ) -> dict[str, Any]: raise NotImplementedError @pytest.fixture def release_info() -> PackageInfo: return PackageInfo( name="mylib", version="1.0", summary="", requires_dist=[], requires_python=">=3.9", files=[ { "file": "mylib-1.0-py3-none-any.whl", "hash": "sha256:dummyhashvalue1234567890abcdef", }, { "file": "mylib-1.0.tar.gz", "hash": "sha256:anotherdummyhashvalueabcdef1234567890", }, ], cache_version=str(CachedRepository.CACHE_VERSION), ) @pytest.fixture def outdated_release_info() -> PackageInfo: return PackageInfo( name="mylib", version="1.0", summary="", requires_dist=[], requires_python=">=3.9", files=[ { "file": "mylib-1.0-py3-none-any.whl", "hash": "sha256:dummyhashvalue1234567890abcdef", } ], cache_version=str(CachedRepository.CACHE_VERSION), ) @pytest.mark.parametrize("disable_cache", [False, True]) def test_get_release_info_cache( release_info: PackageInfo, outdated_release_info: PackageInfo, disable_cache: bool ) -> None: repo = MockCachedRepository("mock", disable_cache=disable_cache) repo._get_release_info = lambda name, version: outdated_release_info.asdict() # type: ignore[method-assign] name = canonicalize_name("mylib") version = Version.parse("1.0") assert len(repo.get_release_info(name, version).files) == 1 # without disable_cache: cached value is returned even if the underlying data has changed # with disable_cache: cached value is ignored and updated data is returned repo._get_release_info = lambda name, version: release_info.asdict() # type: ignore[method-assign] assert len(repo.get_release_info(name, version).files) == ( 2 if disable_cache else 1 ) # after clearing the cache entry, updated data is returned repo.forget(name, version) assert len(repo.get_release_info(name, version).files) == 2 ================================================ FILE: tests/repositories/test_http_repository.py ================================================ from __future__ import annotations import contextlib import shutil from pathlib import Path from typing import TYPE_CHECKING from typing import Any from zipfile import ZipFile import pytest from packaging.metadata import parse_email from poetry.core.packages.utils.link import Link from poetry.inspection.info import PackageInfoError from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupportedError from poetry.repositories.http_repository import HTTPRepository from poetry.utils.helpers import HTTPRangeRequestSupportedError if TYPE_CHECKING: from packaging.utils import NormalizedName from poetry.core.constraints.version import Version from pytest_mock import MockerFixture class MockRepository(HTTPRepository): DIST_FIXTURES = Path(__file__).parent / "fixtures" / "pypi.org" / "dists" def __init__(self, lazy_wheel: bool = True) -> None: super().__init__("foo", "https://foo.com") self._lazy_wheel = lazy_wheel def _get_release_info( self, name: NormalizedName, version: Version ) -> dict[str, Any]: raise NotImplementedError @pytest.mark.parametrize("lazy_wheel", [False, True]) @pytest.mark.parametrize("supports_range_requests", [None, False, True]) def test_get_info_from_wheel( mocker: MockerFixture, lazy_wheel: bool, supports_range_requests: bool | None ) -> None: filename = "poetry_core-1.5.0-py3-none-any.whl" filepath = MockRepository.DIST_FIXTURES / filename with ZipFile(filepath) as zf: metadata, _ = parse_email(zf.read("poetry_core-1.5.0.dist-info/METADATA")) mock_metadata_from_wheel_url = mocker.patch( "poetry.repositories.http_repository.metadata_from_wheel_url", return_value=metadata, ) mock_download = mocker.patch( "poetry.repositories.http_repository.download_file", side_effect=lambda _, dest, *args, **kwargs: shutil.copy(filepath, dest), ) domain = "foo.com" url = f"https://{domain}/{filename}" repo = MockRepository(lazy_wheel) assert not repo._supports_range_requests if lazy_wheel and supports_range_requests is not None: repo._supports_range_requests[domain] = supports_range_requests info = repo._get_info_from_wheel(Link(url)) assert info.name == "poetry-core" assert info.version == "1.5.0" assert info.requires_dist == [ 'importlib-metadata (>=1.7.0) ; python_version < "3.8"' ] if lazy_wheel and supports_range_requests is not False: mock_metadata_from_wheel_url.assert_called_once_with( filename, url, repo.session ) mock_download.assert_not_called() assert repo._supports_range_requests[domain] is True else: mock_metadata_from_wheel_url.assert_not_called() mock_download.assert_called_once_with( url, mocker.ANY, session=repo.session, raise_accepts_ranges=lazy_wheel, max_retries=0, ) if lazy_wheel: assert repo._supports_range_requests[domain] is False else: assert domain not in repo._supports_range_requests def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: """ 1. We know nothing: Try range requests, which are not supported and fall back to complete download. 2. Range requests were not supported so far: We do not try range requests again. 3. Range requests were still not supported so far: We do not try range requests again, but we notice that the response header contains "Accept-Ranges: bytes", so range requests are at least supported for some files, which means we want to try again. 4. Range requests are supported for some files: We try range requests (success). 5. Range requests are supported for some files: We try range requests (failure), but do not forget that range requests are supported for some files. 6. Range requests are supported for some files: We try range requests (success). """ mock_metadata_from_wheel_url = mocker.patch( "poetry.repositories.http_repository.metadata_from_wheel_url" ) mock_download = mocker.patch("poetry.repositories.http_repository.download_file") filename = "poetry_core-1.5.0-py3-none-any.whl" domain = "foo.com" link = Link(f"https://{domain}/{filename}") repo = MockRepository() # 1. range request and download mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupportedError with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) assert mock_metadata_from_wheel_url.call_count == 1 assert mock_download.call_count == 1 assert mock_download.call_args[1]["raise_accepts_ranges"] is False # 2. only download with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) assert mock_metadata_from_wheel_url.call_count == 1 assert mock_download.call_count == 2 assert mock_download.call_args[1]["raise_accepts_ranges"] is True # 3. download and range request mock_metadata_from_wheel_url.side_effect = None mock_download.side_effect = HTTPRangeRequestSupportedError with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) assert mock_metadata_from_wheel_url.call_count == 2 assert mock_download.call_count == 3 assert mock_download.call_args[1]["raise_accepts_ranges"] is True # 4. only range request with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) assert mock_metadata_from_wheel_url.call_count == 3 assert mock_download.call_count == 3 # 5. range request and download mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupportedError mock_download.side_effect = None with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) assert mock_metadata_from_wheel_url.call_count == 4 assert mock_download.call_count == 4 assert mock_download.call_args[1]["raise_accepts_ranges"] is False # 6. only range request mock_metadata_from_wheel_url.side_effect = None with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) assert mock_metadata_from_wheel_url.call_count == 5 assert mock_download.call_count == 4 @pytest.mark.parametrize( "mock_hashes", [ None, {"sha256": "e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84"}, {"md5": "be7589b4902793e66d7d979bd8581591"}, ], ) def test_calculate_sha256( mocker: MockerFixture, mock_hashes: dict[str, Any] | None ) -> None: filename = "poetry_core-1.5.0-py3-none-any.whl" filepath = MockRepository.DIST_FIXTURES / filename mock_download = mocker.patch( "poetry.repositories.http_repository.download_file", side_effect=lambda _, dest, *args, **kwargs: shutil.copy(filepath, dest), ) domain = "foo.com" link = Link(f"https://{domain}/{filename}", hashes=mock_hashes) repo = MockRepository() calculated_hash = repo.calculate_sha256(link) assert mock_download.call_count == 1 assert ( calculated_hash == "sha256:e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84" ) def test_calculate_sha256_defaults_to_sha256_on_md5_errors( mocker: MockerFixture, ) -> None: raised_value_error = False def mock_hashlib_md5_error() -> None: nonlocal raised_value_error raised_value_error = True raise ValueError( "[digital envelope routines: EVP_DigestInit_ex] disabled for FIPS" ) filename = "poetry_core-1.5.0-py3-none-any.whl" filepath = MockRepository.DIST_FIXTURES / filename mock_download = mocker.patch( "poetry.repositories.http_repository.download_file", side_effect=lambda _, dest, *args, **kwargs: shutil.copy(filepath, dest), ) mock_hashlib_md5 = mocker.patch("hashlib.md5", side_effect=mock_hashlib_md5_error) domain = "foo.com" link = Link( f"https://{domain}/{filename}", hashes={"md5": "be7589b4902793e66d7d979bd8581591"}, ) repo = MockRepository() calculated_hash = repo.calculate_sha256(link) assert raised_value_error assert mock_download.call_count == 1 assert mock_hashlib_md5.call_count == 1 assert ( calculated_hash == "sha256:e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84" ) ================================================ FILE: tests/repositories/test_installed_repository.py ================================================ from __future__ import annotations import os import shutil import zipfile from functools import cached_property from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING from typing import NamedTuple import pytest from poetry.repositories.installed_repository import InstalledRepository from poetry.utils._compat import getencoding from poetry.utils.env import EnvManager from poetry.utils.env import MockEnv from poetry.utils.env import VirtualEnv from tests.helpers import with_working_directory if TYPE_CHECKING: from collections.abc import Iterator from poetry.core.packages.package import Package from pytest import LogCaptureFixture from pytest_mock.plugin import MockerFixture from poetry.poetry import Poetry from poetry.utils.env.base_env import PythonVersion from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture(scope="session") def env_dir(tmp_session_working_directory: Path) -> Iterator[Path]: source = Path(__file__).parent / "fixtures" / "installed" target = tmp_session_working_directory / source.name with with_working_directory(source=source, target=target) as path: yield path @pytest.fixture(scope="session") def site_purelib(env_dir: Path) -> Path: return env_dir / "lib" / "python3.7" / "site-packages" @pytest.fixture(scope="session") def site_platlib(env_dir: Path) -> Path: return env_dir / "lib64" / "python3.7" / "site-packages" @pytest.fixture(scope="session") def src_dir(env_dir: Path) -> Path: return env_dir / "src" @pytest.fixture(scope="session") def installed_results( site_purelib: Path, site_platlib: Path, src_dir: Path ) -> list[metadata.PathDistribution]: return [ metadata.PathDistribution(site_purelib / "cleo-0.7.6.dist-info"), metadata.PathDistribution(src_dir / "pendulum" / "pendulum.egg-info"), metadata.PathDistribution( zipfile.Path( # type: ignore[arg-type] site_purelib / "foo-0.1.0-py3.8.egg", "EGG-INFO", ) ), metadata.PathDistribution(site_purelib / "standard-1.2.3.dist-info"), metadata.PathDistribution(site_purelib / "editable-2.3.4.dist-info"), metadata.PathDistribution(site_purelib / "editable-src-dir-2.3.4.dist-info"), metadata.PathDistribution( site_purelib / "editable-with-import-2.3.4.dist-info" ), metadata.PathDistribution(site_platlib / "lib64-2.3.4.dist-info"), metadata.PathDistribution(site_platlib / "bender-2.0.5.dist-info"), metadata.PathDistribution(site_purelib / "git_pep_610-1.2.3.dist-info"), metadata.PathDistribution( site_purelib / "git_pep_610_no_requested_version-1.2.3.dist-info" ), metadata.PathDistribution( site_purelib / "git_pep_610_subdirectory-1.2.3.dist-info" ), metadata.PathDistribution(site_purelib / "url_pep_610-1.2.3.dist-info"), metadata.PathDistribution(site_purelib / "file_pep_610-1.2.3.dist-info"), metadata.PathDistribution(site_purelib / "directory_pep_610-1.2.3.dist-info"), metadata.PathDistribution( site_purelib / "editable_directory_pep_610-1.2.3.dist-info" ), ] @pytest.fixture def env( env_dir: Path, site_purelib: Path, site_platlib: Path, src_dir: Path ) -> MockEnv: class _MockEnv(MockEnv): @cached_property def paths(self) -> dict[str, str]: return { "purelib": site_purelib.as_posix(), "platlib": site_platlib.as_posix(), } @property def sys_path(self) -> list[str]: return [str(path) for path in [env_dir, site_platlib, site_purelib]] return _MockEnv(path=env_dir) @pytest.fixture(autouse=True) def mock_git_info(mocker: MockerFixture) -> None: class GitRepoLocalInfo(NamedTuple): origin: str revision: str mocker.patch( "poetry.vcs.git.Git.info", return_value=GitRepoLocalInfo( origin="https://github.com/sdispater/pendulum.git", revision="bb058f6b78b2d28ef5d9a5e759cfa179a1a713d6", ), ) @pytest.fixture def repository( mocker: MockerFixture, env: MockEnv, installed_results: list[metadata.PathDistribution], ) -> InstalledRepository: mocker.patch( "importlib.metadata.Distribution.discover", return_value=installed_results, ) return InstalledRepository.load(env) def get_package_from_repository( name: str, repository: InstalledRepository ) -> Package | None: for pkg in repository.packages: if pkg.name == name: return pkg return None @pytest.fixture def poetry( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, installed_results: list[metadata.PathDistribution], ) -> Poetry: return project_factory("simple", source=fixture_dir("simple_project")) @pytest.fixture(scope="session") def editable_source_directory_path() -> str: return Path("/path/to/editable").resolve(strict=False).as_posix() @pytest.fixture(scope="session", autouse=(os.name == "nt")) def fix_editable_path_for_windows( site_purelib: Path, editable_source_directory_path: str ) -> None: # we handle this as a special case since in certain scenarios (eg: on Windows GHA runners) # the temp directory is on a different drive causing path resolutions without drive letters # to give inconsistent results at different phases of the test suite execution; additionally # this represents a more realistic scenario editable_pth_file = site_purelib / "editable.pth" editable_pth_file.write_text(editable_source_directory_path, encoding=getencoding()) def test_load_successful( repository: InstalledRepository, installed_results: list[metadata.PathDistribution] ) -> None: assert len(repository.packages) == len(installed_results) def test_load_successful_with_invalid_distribution( caplog: LogCaptureFixture, mocker: MockerFixture, env: MockEnv, tmp_path: Path, installed_results: list[metadata.PathDistribution], ) -> None: invalid_dist_info = tmp_path / "site-packages" / "invalid-0.1.0.dist-info" invalid_dist_info.mkdir(parents=True) mocker.patch( "importlib.metadata.Distribution.discover", return_value=[*installed_results, metadata.PathDistribution(invalid_dist_info)], ) repository_with_invalid_distribution = InstalledRepository.load(env) assert len(repository_with_invalid_distribution.packages) == len(installed_results) assert len(caplog.messages) == 1 message = caplog.messages[0] assert message.startswith("Project environment contains an invalid distribution") assert str(invalid_dist_info) in message def test_loads_in_correct_sys_path_order( tmp_path: Path, current_python: PythonVersion, fixture_dir: FixtureDirGetter ) -> None: path1 = tmp_path / "path1" path1.mkdir() path2 = tmp_path / "path2" path2.mkdir() env = MockEnv(path=tmp_path, sys_path=[str(path1), str(path2)]) fixtures = fixture_dir("project_plugins") dist_info_1 = "my_application_plugin-1.0.dist-info" dist_info_2 = "my_application_plugin-2.0.dist-info" dist_info_other = "my_other_plugin-1.0.dist-info" shutil.copytree(fixtures / dist_info_1, path1 / dist_info_1) shutil.copytree(fixtures / dist_info_2, path2 / dist_info_2) shutil.copytree(fixtures / dist_info_other, path2 / dist_info_other) repo = InstalledRepository.load(env) assert {f"{p.name} {p.version}" for p in repo.packages} == { "my-application-plugin 1.0", "my-other-plugin 1.0", } def test_load_ensure_isolation(repository: InstalledRepository) -> None: package = get_package_from_repository("attrs", repository) assert package is None def test_load_standard_package(repository: InstalledRepository) -> None: cleo = get_package_from_repository("cleo", repository) assert cleo is not None assert cleo.name == "cleo" assert cleo.version.text == "0.7.6" assert ( cleo.description == "Cleo allows you to create beautiful and testable command-line interfaces." ) foo = get_package_from_repository("foo", repository) assert foo is not None assert foo.version.text == "0.1.0" def test_load_git_package(repository: InstalledRepository) -> None: pendulum = get_package_from_repository("pendulum", repository) assert pendulum is not None assert pendulum.name == "pendulum" assert pendulum.version.text == "2.0.5" assert pendulum.description == "Python datetimes made easy" assert pendulum.source_type == "git" assert pendulum.source_url in [ "git@github.com:sdispater/pendulum.git", "https://github.com/sdispater/pendulum.git", ] assert pendulum.source_reference == "bb058f6b78b2d28ef5d9a5e759cfa179a1a713d6" def test_load_git_package_pth(repository: InstalledRepository) -> None: bender = get_package_from_repository("bender", repository) assert bender is not None assert bender.name == "bender" assert bender.version.text == "2.0.5" assert bender.source_type == "git" def test_load_platlib_package(repository: InstalledRepository) -> None: lib64 = get_package_from_repository("lib64", repository) assert lib64 is not None assert lib64.name == "lib64" assert lib64.version.text == "2.3.4" def test_load_editable_package( repository: InstalledRepository, editable_source_directory_path: str ) -> None: # test editable package with text .pth file editable = get_package_from_repository("editable", repository) assert editable is not None assert editable.name == "editable" assert editable.version.text == "2.3.4" assert editable.source_type == "directory" assert editable.source_url == editable_source_directory_path def test_load_editable_src_dir_package( repository: InstalledRepository, editable_source_directory_path: str ) -> None: # test editable package with src layout with text .pth file editable = get_package_from_repository("editable-src-dir", repository) assert editable is not None assert editable.name == "editable-src-dir" assert editable.version.text == "2.3.4" assert editable.source_type == "directory" assert editable.source_url == editable_source_directory_path def test_load_editable_with_import_package(repository: InstalledRepository) -> None: # test editable package with executable .pth file editable = get_package_from_repository("editable-with-import", repository) assert editable is not None assert editable.name == "editable-with-import" assert editable.version.text == "2.3.4" assert editable.source_type is None assert editable.source_url is None def test_load_standard_package_with_pth_file(repository: InstalledRepository) -> None: # test standard packages with .pth file is not treated as editable standard = get_package_from_repository("standard", repository) assert standard is not None assert standard.name == "standard" assert standard.version.text == "1.2.3" assert standard.source_type is None assert standard.source_url is None def test_load_pep_610_compliant_git_packages(repository: InstalledRepository) -> None: package = get_package_from_repository("git-pep-610", repository) assert package is not None assert package.name == "git-pep-610" assert package.version.text == "1.2.3" assert package.source_type == "git" assert package.source_url == "https://github.com/demo/git-pep-610.git" assert package.source_reference == "my-branch" assert package.source_resolved_reference == "123456" def test_load_pep_610_compliant_git_packages_no_requested_version( repository: InstalledRepository, ) -> None: package = get_package_from_repository( "git-pep-610-no-requested-version", repository ) assert package is not None assert package.name == "git-pep-610-no-requested-version" assert package.version.text == "1.2.3" assert package.source_type == "git" assert ( package.source_url == "https://github.com/demo/git-pep-610-no-requested-version.git" ) assert package.source_resolved_reference == "123456" assert package.source_reference == package.source_resolved_reference def test_load_pep_610_compliant_git_packages_with_subdirectory( repository: InstalledRepository, ) -> None: package = get_package_from_repository("git-pep-610-subdirectory", repository) assert package is not None assert package.name == "git-pep-610-subdirectory" assert package.version.text == "1.2.3" assert package.source_type == "git" assert package.source_url == "https://github.com/demo/git-pep-610-subdirectory.git" assert package.source_reference == "my-branch" assert package.source_resolved_reference == "123456" assert package.source_subdirectory == "subdir" def test_load_pep_610_compliant_url_packages(repository: InstalledRepository) -> None: package = get_package_from_repository("url-pep-610", repository) assert package is not None assert package.name == "url-pep-610" assert package.version.text == "1.2.3" assert package.source_type == "url" assert ( package.source_url == "https://mock.pythonhosted.org/distributions/url-pep-610-1.2.3.tar.gz" ) def test_load_pep_610_compliant_file_packages(repository: InstalledRepository) -> None: package = get_package_from_repository("file-pep-610", repository) assert package is not None assert package.name == "file-pep-610" assert package.version.text == "1.2.3" assert package.source_type == "file" assert package.source_url == "/path/to/distributions/file-pep-610-1.2.3.tar.gz" def test_load_pep_610_compliant_directory_packages( repository: InstalledRepository, ) -> None: package = get_package_from_repository("directory-pep-610", repository) assert package is not None assert package.name == "directory-pep-610" assert package.version.text == "1.2.3" assert package.source_type == "directory" assert package.source_url == "/path/to/distributions/directory-pep-610" assert not package.develop def test_load_pep_610_compliant_editable_directory_packages( repository: InstalledRepository, ) -> None: package = get_package_from_repository("editable-directory-pep-610", repository) assert package is not None assert package.name == "editable-directory-pep-610" assert package.version.text == "1.2.3" assert package.source_type == "directory" assert package.source_url == "/path/to/distributions/directory-pep-610" assert package.develop @pytest.mark.parametrize("with_system_site_packages", [False, True]) def test_system_site_packages( tmp_path: Path, mocker: MockerFixture, poetry: Poetry, site_purelib: Path, with_system_site_packages: bool, ) -> None: venv_path = tmp_path / "venv" site_path = tmp_path / "site" cleo_dist_info = "cleo-0.7.6.dist-info" shutil.copytree(site_purelib / cleo_dist_info, site_path / cleo_dist_info) EnvManager(poetry).build_venv( path=venv_path, flags={"system-site-packages": with_system_site_packages} ) env = VirtualEnv(venv_path) standard_dist_info = "standard-1.2.3.dist-info" shutil.copytree(site_purelib / standard_dist_info, env.purelib / standard_dist_info) orig_sys_path = env.sys_path if with_system_site_packages: # on some environments, there could be multiple system-site, filter out those and inject our test site mocker.patch( "poetry.utils.env.virtual_env.VirtualEnv.sys_path", [p for p in orig_sys_path if p.startswith(str(tmp_path))] + [str(site_path)], ) mocker.patch( "poetry.utils.env.generic_env.GenericEnv.get_paths", return_value={"purelib": str(site_path)}, ) installed_repository = InstalledRepository.load(env) expected_system_site_packages = {"cleo"} if with_system_site_packages else set() expected_packages = {"standard"} expected_packages |= expected_system_site_packages assert {p.name for p in installed_repository.packages} == expected_packages assert { p.name for p in installed_repository.system_site_packages } == expected_system_site_packages def test_system_site_packages_source_type( tmp_path: Path, mocker: MockerFixture, poetry: Poetry, site_purelib: Path ) -> None: """ The source type of system site packages must not be falsely identified as "directory". """ venv_path = tmp_path / "venv" site_path = tmp_path / "site" for dist_info in {"cleo-0.7.6.dist-info", "directory_pep_610-1.2.3.dist-info"}: shutil.copytree(site_purelib / dist_info, site_path / dist_info) mocker.patch("poetry.utils.env.virtual_env.VirtualEnv.sys_path", [str(site_path)]) mocker.patch( "poetry.utils.env.generic_env.GenericEnv.get_paths", return_value={"purelib": str(site_path)}, ) EnvManager(poetry).build_venv(path=venv_path, flags={"system-site-packages": True}) env = VirtualEnv(venv_path) installed_repository = InstalledRepository.load(env) assert installed_repository.packages == installed_repository.system_site_packages source_types = { package.name: package.source_type for package in installed_repository.packages } assert source_types == {"cleo": None, "directory-pep-610": "directory"} def test_pipx_shared_lib_site_packages( tmp_path: Path, poetry: Poetry, site_purelib: Path, caplog: LogCaptureFixture, ) -> None: """ Simulate pipx shared/lib/site-packages which is not relative to the venv path. """ venv_path = tmp_path / "venv" shared_lib_site_path = tmp_path / "site" env = MockEnv( path=venv_path, sys_path=[str(venv_path / "purelib"), str(shared_lib_site_path)] ) dist_info = "cleo-0.7.6.dist-info" shutil.copytree(site_purelib / dist_info, shared_lib_site_path / dist_info) installed_repository = InstalledRepository.load(env) assert len(installed_repository.packages) == 1 assert installed_repository.system_site_packages == [] cleo_package = installed_repository.packages[0] cleo_package.to_dependency() # There must not be a warning # that the package does not seem to be a valid Python package. assert caplog.messages == [] assert cleo_package.source_type is None ================================================ FILE: tests/repositories/test_legacy_repository.py ================================================ from __future__ import annotations import base64 import re from typing import TYPE_CHECKING from typing import Any import pytest import requests from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency from poetry.core.packages.utils.link import Link from poetry.factory import Factory from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.exceptions import RepositoryError from poetry.repositories.legacy_repository import LegacyRepository if TYPE_CHECKING: from collections.abc import Callable import responses from pytest import MonkeyPatch from pytest_mock import MockerFixture from poetry.config.config import Config from tests.repositories.fixtures.legacy import TestLegacyRepository from tests.types import DistributionHashGetter @pytest.fixture(autouse=True) def _use_simple_keyring(with_simple_keyring: None) -> None: pass def test_page_relative_links_path_are_correct( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository page = repo.get_page("relative") assert page is not None for link in page.links: assert link.netloc == "legacy.foo.bar" assert link.path.startswith("/relative/poetry") def test_page_absolute_links_path_are_correct( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository page = repo.get_page("absolute") assert page is not None for link in page.links: assert link.netloc == "files.pythonhosted.org" assert link.path.startswith("/packages/") def test_page_clean_link(legacy_repository: LegacyRepository) -> None: repo = legacy_repository page = repo.get_page("relative") assert page is not None cleaned = page.clean_link('https://legacy.foo.bar/test /the"/cleaning\0') assert cleaned == "https://legacy.foo.bar/test%20/the%22/cleaning%00" def test_page_invalid_version_link(legacy_repository: LegacyRepository) -> None: repo = legacy_repository page = repo.get_page("invalid-version") assert page is not None links = list(page.links) assert len(links) == 1 versions = list(page.versions(canonicalize_name("poetry"))) assert len(versions) == 1 assert versions[0].to_string() == "0.1.0" packages = list(page.packages) assert len(packages) == 1 assert packages[0].name == "poetry" assert packages[0].version.to_string() == "0.1.0" def test_page_filters_out_invalid_package_names( legacy_repository_with_extra_packages: LegacyRepository, get_legacy_dist_url: Callable[[str], str], dist_hash_getter: DistributionHashGetter, ) -> None: repo = legacy_repository_with_extra_packages packages = repo.find_packages(Factory.create_dependency("pytest", "*")) assert len(packages) == 1 assert packages[0].name == "pytest" assert packages[0].version == Version.parse("3.5.0") package = repo.package("pytest", Version.parse("3.5.0")) assert package.files == [ { "file": filename, "hash": f"sha256:{dist_hash_getter(filename).sha256}", "url": get_legacy_dist_url(filename), } for filename in [ f"{package.name}-{package.version}-py2.py3-none-any.whl", f"{package.name}-{package.version}.tar.gz", ] ] def test_sdist_format_support(legacy_repository: LegacyRepository) -> None: repo = legacy_repository page = repo.get_page("relative") assert page is not None bz2_links = list(filter(lambda link: link.ext == ".tar.bz2", page.links)) assert len(bz2_links) == 1 assert bz2_links[0].filename == "poetry-0.1.1.tar.bz2" def test_missing_version(legacy_repository: LegacyRepository) -> None: repo = legacy_repository with pytest.raises(PackageNotFoundError): repo._get_release_info( canonicalize_name("missing_version"), Version.parse("1.1.0") ) def test_get_package_information_fallback_read_setup( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package("jupyter", Version.parse("1.0.0")) assert package.source_type == "legacy" assert package.source_reference == repo.name assert package.source_url == repo.url assert package.name == "jupyter" assert package.version.text == "1.0.0" assert ( package.description == "Jupyter metapackage. Install all the Jupyter components in one go." ) def test_get_package_information_pep_658( mocker: MockerFixture, legacy_repository: LegacyRepository ) -> None: repo = legacy_repository isort_package = repo.package("isort", Version.parse("4.3.4")) spy = mocker.spy(repo, "_get_info_from_metadata") try: package = repo.package("isort-metadata", Version.parse("4.3.4")) except FileNotFoundError: pytest.fail("Metadata was not successfully retrieved") else: assert spy.call_count > 0 assert spy.spy_return is not None assert package.source_type == isort_package.source_type == "legacy" assert package.source_reference == isort_package.source_reference == repo.name assert package.source_url == isort_package.source_url == repo.url assert package.name == "isort-metadata" assert package.version.text == isort_package.version.text == "4.3.4" assert package.description == isort_package.description assert ( package.requires == isort_package.requires == [Dependency("futures", "*")] ) assert ( str(package.python_constraint) == str(isort_package.python_constraint) == ">=2.7,<3.0.dev0 || >=3.4.dev0" ) def test_get_package_information_skips_dependencies_with_invalid_constraints( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package("python-language-server", Version.parse("0.21.2")) assert package.name == "python-language-server" assert package.version.text == "0.21.2" assert ( package.description == "Python Language Server for the Language Server Protocol" ) assert len(package.requires) == 25 assert sorted( (r for r in package.requires if not r.is_optional()), key=lambda r: r.name ) == [ Dependency("configparser", "*"), Dependency("future", ">=0.14.0"), Dependency("futures", "*"), Dependency("jedi", ">=0.12"), Dependency("pluggy", "*"), Dependency("python-jsonrpc-server", "*"), ] all_extra = package.extras[canonicalize_name("all")] # rope>-0.10.5 should be discarded assert sorted(all_extra, key=lambda r: r.name) == [ Dependency("autopep8", "*"), Dependency("mccabe", "*"), Dependency("pycodestyle", "*"), Dependency("pydocstyle", ">=2.0.0"), Dependency("pyflakes", ">=1.6.0"), Dependency("yapf", "*"), ] def test_package_not_canonicalized(legacy_repository: LegacyRepository) -> None: repo = legacy_repository package = repo.package("discord.py", Version.parse("2.0.0")) assert package.name == "discord-py" assert package.pretty_name == "discord.py" def test_find_packages_no_prereleases(legacy_repository: LegacyRepository) -> None: repo = legacy_repository packages = repo.find_packages(Factory.create_dependency("pyyaml", "*")) assert len(packages) == 1 assert packages[0].source_type == "legacy" assert packages[0].source_reference == repo.name assert packages[0].source_url == repo.url @pytest.mark.parametrize( ["constraint", "count"], [("*", 1), (">=1", 1), ("<=18", 0), (">=19.0.0a0", 1)] ) def test_find_packages_only_prereleases( constraint: str, count: int, legacy_repository: LegacyRepository ) -> None: repo = legacy_repository packages = repo.find_packages(Factory.create_dependency("black", constraint)) assert len(packages) == count if count >= 0: for package in packages: assert package.source_type == "legacy" assert package.source_reference == repo.name assert package.source_url == repo.url @pytest.mark.parametrize( ["constraint", "expected"], [ # yanked 21.11b0 is ignored except for pinned version ("*", ["19.10b0"]), (">=19.0a0", ["19.10b0"]), (">=20.0a0", []), (">=21.11b0", []), ("==21.11b0", ["21.11b0"]), ], ) def test_find_packages_yanked( constraint: str, expected: list[str], legacy_repository: LegacyRepository ) -> None: repo = legacy_repository packages = repo.find_packages(Factory.create_dependency("black", constraint)) assert [str(p.version) for p in packages] == expected def test_get_package_information_chooses_correct_distribution( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package("isort", Version.parse("4.3.4")) assert package.name == "isort" assert package.version.text == "4.3.4" assert package.requires == [Dependency("futures", "*")] futures_dep = package.requires[0] assert futures_dep.python_versions == "~2.7" def test_get_package_information_includes_python_requires( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package("futures", Version.parse("3.2.0")) assert package.name == "futures" assert package.version.text == "3.2.0" assert package.python_versions == ">=2.6, <3" def test_get_package_information_includes_files( legacy_repository: TestLegacyRepository, dist_hash_getter: DistributionHashGetter, get_legacy_dist_url: Callable[[str], str], get_legacy_dist_size_and_upload_time: Callable[ [str], tuple[int | None, str | None] ], ) -> None: repo = legacy_repository package = repo.package("futures", Version.parse("3.2.0")) expected: list[dict[str, Any]] = [ { "file": filename, "hash": f"sha256:{dist_hash_getter(filename).sha256}", "url": get_legacy_dist_url(filename), } for filename in [ f"{package.name}-{package.version}-py2-none-any.whl", f"{package.name}-{package.version}.tar.gz", ] ] if repo.json: for file_info in expected: size, upload_time = get_legacy_dist_size_and_upload_time(file_info["file"]) if size is not None: file_info["size"] = size if upload_time is not None: file_info["upload_time"] = upload_time assert package.files == expected def test_get_package_information_sets_appropriate_python_versions_if_wheels_only( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package("futures", Version.parse("3.2.0")) assert package.name == "futures" assert package.version.text == "3.2.0" assert package.python_versions == ">=2.6, <3" def test_get_package_from_both_py2_and_py3_specific_wheels( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package("ipython", Version.parse("5.7.0")) assert package.name == "ipython" assert package.version.text == "5.7.0" assert package.python_versions == "*" assert len(package.requires) == 41 expected = [ Dependency("appnope", "*"), Dependency("backports.shutil-get-terminal-size", "*"), Dependency("colorama", "*"), Dependency("decorator", "*"), Dependency("pathlib2", "*"), Dependency("pexpect", "*"), Dependency("pickleshare", "*"), Dependency("prompt-toolkit", ">=1.0.4,<2.0.0"), Dependency("pygments", "*"), Dependency("setuptools", ">=18.5"), Dependency("simplegeneric", ">0.8"), Dependency("traitlets", ">=4.2"), Dependency("win-unicode-console", ">=0.5"), ] required = [r for r in package.requires if not r.is_optional()] assert required == expected assert str(required[1].marker) == 'python_version == "2.7"' assert ( str(required[12].marker) == 'sys_platform == "win32" and python_version < "3.6"' ) assert ( str(required[4].marker) == 'python_version == "2.7" or python_version == "3.3"' ) assert str(required[5].marker) == 'sys_platform != "win32"' def test_get_package_from_both_py2_and_py3_specific_wheels_python_constraint( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package("poetry-test-py2-py3-metadata-merge", Version.parse("0.1.0")) assert package.name == "poetry-test-py2-py3-metadata-merge" assert package.version.text == "0.1.0" assert package.python_versions == ">=2.7,<2.8 || >=3.7,<4.0" def test_get_package_with_dist_and_universal_py3_wheel( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package("ipython", Version.parse("7.5.0")) assert package.name == "ipython" assert package.version.text == "7.5.0" assert package.python_versions == ">=3.5" expected = [ Dependency("appnope", "*"), Dependency("backcall", "*"), Dependency("colorama", "*"), Dependency("decorator", "*"), Dependency("jedi", ">=0.10"), Dependency("pexpect", "*"), Dependency("pickleshare", "*"), Dependency("prompt-toolkit", ">=2.0.0,<2.1.0"), Dependency("pygments", "*"), Dependency("setuptools", ">=18.5"), Dependency("traitlets", ">=4.2"), Dependency("typing", "*"), Dependency("win-unicode-console", ">=0.5"), ] required = [r for r in package.requires if not r.is_optional()] assert sorted(required, key=lambda dep: dep.name) == expected def test_get_package_retrieves_non_sha256_hashes( legacy_repository: TestLegacyRepository, dist_hash_getter: DistributionHashGetter, get_legacy_dist_url: Callable[[str], str], get_legacy_dist_size_and_upload_time: Callable[ [str], tuple[int | None, str | None] ], ) -> None: repo = legacy_repository package = repo.package("ipython", Version.parse("7.5.0")) expected: list[dict[str, Any]] = [ { "file": filename, "hash": f"sha256:{dist_hash_getter(filename).sha256}", "url": get_legacy_dist_url(filename), } for filename in [ f"{package.name}-{package.version}-py3-none-any.whl", f"{package.name}-{package.version}.tar.gz", ] ] if repo.json: for file_info in expected: size, upload_time = get_legacy_dist_size_and_upload_time(file_info["file"]) if size is not None: file_info["size"] = size if upload_time is not None: file_info["upload_time"] = upload_time assert package.files == expected def test_get_package_retrieves_non_sha256_hashes_mismatching_known_hash( legacy_repository: TestLegacyRepository, dist_hash_getter: DistributionHashGetter, get_legacy_dist_url: Callable[[str], str], get_legacy_dist_size_and_upload_time: Callable[ [str], tuple[int | None, str | None] ], ) -> None: repo = legacy_repository package = repo.package("ipython", Version.parse("5.7.0")) expected: list[dict[str, Any]] = [ { "file": "ipython-5.7.0-py2-none-any.whl", # in the links provided by the legacy repository, this file only has a md5 hash, # the sha256 is generated on the fly "hash": f"sha256:{dist_hash_getter('ipython-5.7.0-py2-none-any.whl').sha256}", "url": get_legacy_dist_url("ipython-5.7.0-py2-none-any.whl"), }, { "file": "ipython-5.7.0-py3-none-any.whl", "hash": f"sha256:{dist_hash_getter('ipython-5.7.0-py3-none-any.whl').sha256}", "url": get_legacy_dist_url("ipython-5.7.0-py3-none-any.whl"), }, { "file": "ipython-5.7.0.tar.gz", "hash": f"sha256:{dist_hash_getter('ipython-5.7.0.tar.gz').sha256}", "url": get_legacy_dist_url("ipython-5.7.0.tar.gz"), }, ] if repo.json: for file_info in expected: size, upload_time = get_legacy_dist_size_and_upload_time(file_info["file"]) if size is not None: file_info["size"] = size if upload_time is not None: file_info["upload_time"] = upload_time assert package.files == expected def test_get_package_retrieves_packages_with_no_hashes( legacy_repository: LegacyRepository, dist_hash_getter: DistributionHashGetter, get_legacy_dist_url: Callable[[str], str], ) -> None: repo = legacy_repository package = repo.package("jupyter", Version.parse("1.0.0")) assert [ { "file": filename, "hash": f"sha256:{dist_hash_getter(filename).sha256}", "url": get_legacy_dist_url(filename), } for filename in [ f"{package.name}-{package.version}.tar.gz", ] ] == package.files @pytest.mark.parametrize( "package_name, version, yanked, yanked_reason", [ ("black", "19.10b0", False, ""), ("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."), ], ) def test_package_yanked( package_name: str, version: str, yanked: bool, yanked_reason: str, legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package(package_name, Version.parse(version)) assert package.name == package_name assert str(package.version) == version assert package.yanked is yanked assert package.yanked_reason == yanked_reason def test_package_partial_yank( legacy_repository_html: LegacyRepository, legacy_repository_partial_yank: LegacyRepository, ) -> None: repo = legacy_repository_html package = repo.package("futures", Version.parse("3.2.0")) assert len(package.files) == 2 repo = legacy_repository_partial_yank package = repo.package("futures", Version.parse("3.2.0")) assert len(package.files) == 1 assert package.files[0]["file"].endswith(".tar.gz") @pytest.mark.parametrize( "package_name, version, yanked, yanked_reason", [ ("black", "19.10b0", False, ""), ("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."), ], ) def test_find_links_for_package_yanked( package_name: str, version: str, yanked: bool, yanked_reason: str, legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository package = repo.package(package_name, Version.parse(version)) links = repo.find_links_for_package(package) assert len(links) == 2 for link in links: assert link.yanked == yanked assert link.yanked_reason == yanked_reason def test_cached_or_downloaded_file_supports_trailing_slash( legacy_repository: LegacyRepository, ) -> None: repo = legacy_repository with repo._cached_or_downloaded_file( Link("https://files.pythonhosted.org/pytest-3.5.0-py2.py3-none-any.whl/") ) as filepath: assert filepath.name == "pytest-3.5.0-py2.py3-none-any.whl" class MockHttpRepository(LegacyRepository): def __init__( self, endpoint_responses: dict[str, int], http: responses.RequestsMock ) -> None: base_url = "http://legacy.foo.bar" super().__init__("legacy", url=base_url, disable_cache=True) for endpoint, response in endpoint_responses.items(): url = base_url + endpoint http.get(url, status=response) def test_get_200_returns_page(http: responses.RequestsMock) -> None: repo = MockHttpRepository({"/foo/": 200}, http) _ = repo.get_page("foo") @pytest.mark.parametrize("status_code", [401, 403, 404]) def test_get_40x_and_returns_none( http: responses.RequestsMock, status_code: int ) -> None: repo = MockHttpRepository({"/foo/": status_code}, http) with pytest.raises(PackageNotFoundError): repo.get_page("foo") def test_get_5xx_raises( http: responses.RequestsMock, disable_http_status_force_list: None ) -> None: repo = MockHttpRepository({"/foo/": 500}, http) with pytest.raises(RepositoryError): repo.get_page("foo") def test_get_redirected_response_url( http: responses.RequestsMock, monkeypatch: MonkeyPatch ) -> None: repo = MockHttpRepository({"/foo/": 200}, http) redirect_url = "http://legacy.redirect.bar" def get_mock(*args: Any, **kwargs: Any) -> requests.Response: response = requests.Response() response.status_code = 200 response.url = redirect_url + "/foo" return response monkeypatch.setattr(repo.session, "get", get_mock) page = repo.get_page("foo") assert page is not None assert page._url == "http://legacy.redirect.bar/foo" def test_get_page_prefers_json(http: responses.RequestsMock) -> None: repo = MockHttpRepository({"/foo/": 200}, http) _ = repo.get_page("foo") accepted = [ item.strip() for item in http.calls[-1].request.headers.get("Accept", "").split(",") ] preferred = [item for item in accepted if "q=0" not in item.split(";")[-1]] assert preferred == ["application/vnd.pypi.simple.v1+json"] assert any("*/*" in item for item in accepted) def test_root_page_prefers_json(http: responses.RequestsMock) -> None: repo = MockHttpRepository({"/": 200}, http) _ = repo.root_page accepted = [ item.strip() for item in http.calls[-1].request.headers.get("Accept", "").split(",") ] preferred = [item for item in accepted if "q=0" not in item.split(";")[-1]] assert preferred == ["application/vnd.pypi.simple.v1+json"] assert any("*/*" in item for item in accepted) @pytest.mark.parametrize( ("repositories",), [ ({},), # ensure path is respected ({"publish": {"url": "https://foo.bar/legacy"}},), # ensure path length does not give incorrect results ({"publish": {"url": "https://foo.bar/upload/legacy"}},), ], ) def test_authenticator_with_implicit_repository_configuration( http: responses.RequestsMock, config: Config, repositories: dict[str, dict[str, str]], ) -> None: http.get( re.compile("^https?://foo.bar/(.+?)$"), ) config.merge( { "repositories": repositories, "http-basic": { "source": {"username": "foo", "password": "bar"}, "publish": {"username": "baz", "password": "qux"}, }, } ) repo = LegacyRepository(name="source", url="https://foo.bar/simple", config=config) repo.get_page("/foo") request = http.calls[-1].request basic_auth = base64.b64encode(b"foo:bar").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" ================================================ FILE: tests/repositories/test_lockfile_repository.py ================================================ from __future__ import annotations from copy import deepcopy import pytest from poetry.core.packages.package import Package from poetry.repositories.lockfile_repository import LockfileRepository @pytest.fixture(scope="module") def packages() -> list[Package]: return [ Package("a", "1.0", source_type="url", source_url="https://example.org/a.whl"), Package("a", "1.0"), Package( "a", "1.0", source_type="url", source_url="https://example.org/a-1.whl" ), ] def test_has_package(packages: list[Package]) -> None: url_package, pypi_package, url_package_2 = packages repo = LockfileRepository() assert not repo.has_package(url_package) repo.add_package(url_package) assert not repo.has_package(pypi_package) repo.add_package(pypi_package) assert not repo.has_package(url_package_2) repo.add_package(url_package_2) assert len(repo.packages) == 3 assert repo.has_package(deepcopy(url_package)) assert repo.has_package(deepcopy(pypi_package)) assert repo.has_package(deepcopy(url_package_2)) ================================================ FILE: tests/repositories/test_pypi_repository.py ================================================ from __future__ import annotations from io import BytesIO from typing import TYPE_CHECKING from typing import Any import pytest from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency from requests.exceptions import TooManyRedirects from requests.models import Response from poetry.factory import Factory from poetry.repositories.pypi_repository import PyPiRepository if TYPE_CHECKING: from collections.abc import Callable from pytest_mock import MockerFixture from tests.types import DistributionHashGetter @pytest.fixture(autouse=True) def _use_simple_keyring(with_simple_keyring: None) -> None: pass def test_find_packages(pypi_repository: PyPiRepository) -> None: repo = pypi_repository packages = repo.find_packages(Factory.create_dependency("requests", "~2.18.0")) assert len(packages) == 5 def test_find_packages_with_prereleases(pypi_repository: PyPiRepository) -> None: repo = pypi_repository packages = repo.find_packages(Factory.create_dependency("toga", ">=0.3.0.dev2")) assert len(packages) == 2 def test_find_packages_does_not_select_prereleases_if_not_allowed( pypi_repository: PyPiRepository, ) -> None: repo = pypi_repository packages = repo.find_packages(Factory.create_dependency("pyyaml", "*")) assert len(packages) == 1 @pytest.mark.parametrize( ["constraint", "count"], [("*", 1), (">=1", 1), ("<=18", 0), (">=19.0.0a0", 1)] ) def test_find_packages_only_prereleases( constraint: str, count: int, pypi_repository: PyPiRepository ) -> None: repo = pypi_repository packages = repo.find_packages(Factory.create_dependency("black", constraint)) assert len(packages) == count @pytest.mark.parametrize( ["constraint", "expected"], [ # yanked 21.11b0 is ignored except for pinned version ("*", ["19.10b0"]), (">=19.0a0", ["19.10b0"]), (">=20.0a0", []), (">=21.11b0", []), ("==21.11b0", ["21.11b0"]), ], ) def test_find_packages_yanked( constraint: str, expected: list[str], pypi_repository: PyPiRepository ) -> None: repo = pypi_repository packages = repo.find_packages(Factory.create_dependency("black", constraint)) assert [str(p.version) for p in packages] == expected def test_package( pypi_repository: PyPiRepository, dist_hash_getter: DistributionHashGetter, get_pypi_file_info: Callable[[str], dict[str, Any]], ) -> None: repo = pypi_repository package = repo.package("requests", Version.parse("2.18.4")) assert package.name == "requests" assert len(package.requires) == 9 assert len([r for r in package.requires if r.is_optional()]) == 5 assert len(package.extras[canonicalize_name("security")]) == 3 assert len(package.extras[canonicalize_name("socks")]) == 2 assert package.files == [ { "file": filename, "hash": f"sha256:{dist_hash_getter(filename).sha256}", "url": (file_info := get_pypi_file_info(filename))["url"], "size": file_info["size"], "upload_time": file_info["upload_time_iso_8601"], } for filename in [ f"{package.name}-{package.version}-py2.py3-none-any.whl", f"{package.name}-{package.version}.tar.gz", ] ] win_inet = package.extras[canonicalize_name("socks")][1] assert win_inet.name == "win-inet-pton" assert win_inet.python_versions in {"~2.7 || ~2.6", ">=2.6 <2.8"} # Different versions of poetry-core simplify the following marker differently, # either is fine. marker1 = ( 'sys_platform == "win32" and (python_version == "2.7" or python_version ==' ' "2.6") and extra == "socks"' ) marker2 = ( 'sys_platform == "win32" and python_version == "2.7" and extra == "socks" or' ' sys_platform == "win32" and python_version == "2.6" and extra == "socks"' ) marker3 = ( 'sys_platform == "win32" and python_version >= "2.6" and python_version < ' '"2.8" and extra == "socks"' ) assert str(win_inet.marker) in {marker1, marker2, marker3} @pytest.mark.parametrize( "package_name, version, yanked, yanked_reason", [ ("black", "19.10b0", False, ""), ("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."), ], ) def test_package_yanked( package_name: str, version: str, yanked: bool, yanked_reason: str, pypi_repository: PyPiRepository, ) -> None: repo = pypi_repository package = repo.package(package_name, Version.parse(version)) assert package.name == package_name assert str(package.version) == version assert package.yanked is yanked assert package.yanked_reason == yanked_reason @pytest.mark.parametrize("fallback", [False, True]) def test_package_yanked_no_dependencies( pypi_repository: PyPiRepository, fallback: bool ) -> None: repo = pypi_repository repo._fallback = fallback package = repo.package("isodate", Version.parse("0.7.0")) assert package.name == "isodate" assert str(package.version) == "0.7.0" assert package.yanked is True assert package.yanked_reason == "fails for py2.7 but is not marked as py3 only." def test_package_not_canonicalized(pypi_repository: PyPiRepository) -> None: repo = pypi_repository package = repo.package("discord.py", Version.parse("2.0.0")) assert package.name == "discord-py" assert package.pretty_name == "discord.py" @pytest.mark.parametrize( "package_name, version, yanked, yanked_reason", [ ("black", "19.10b0", False, ""), ("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."), ], ) def test_find_links_for_package_yanked( package_name: str, version: str, yanked: bool, yanked_reason: str, pypi_repository: PyPiRepository, ) -> None: repo = pypi_repository package = repo.package(package_name, Version.parse(version)) links = repo.find_links_for_package(package) assert len(links) == 2 for link in links: assert link.yanked == yanked assert link.yanked_reason == yanked_reason def test_fallback_on_downloading_packages(pypi_repository: PyPiRepository) -> None: repo = pypi_repository repo._fallback = True package = repo.package("jupyter", Version.parse("1.0.0")) assert package.name == "jupyter" assert len(package.requires) == 6 dependency_names = sorted(dep.name for dep in package.requires) assert dependency_names == [ "ipykernel", "ipywidgets", "jupyter-console", "nbconvert", "notebook", "qtconsole", ] def test_fallback_inspects_sdist_first_if_no_matching_wheels_can_be_found( pypi_repository: PyPiRepository, ) -> None: repo = pypi_repository repo._fallback = True package = repo.package("isort", Version.parse("4.3.4")) assert package.name == "isort" assert len(package.requires) == 1 dep = package.requires[0] assert dep.name == "futures" assert dep.python_versions == "~2.7" def test_fallback_pep_658_metadata( mocker: MockerFixture, pypi_repository: PyPiRepository ) -> None: repo = pypi_repository repo._fallback = True spy = mocker.spy(repo, "_get_info_from_metadata") try: package = repo.package("isort-metadata", Version.parse("4.3.4")) except FileNotFoundError: pytest.fail("Metadata was not successfully retrieved") else: assert spy.call_count > 0 assert spy.spy_return is not None assert package.name == "isort-metadata" assert len(package.requires) == 1 dep = package.requires[0] assert dep.name == "futures" assert dep.python_versions == "~2.7" def test_pypi_repository_supports_reading_bz2_files( pypi_repository: PyPiRepository, ) -> None: repo = pypi_repository repo._fallback = True package = repo.package("twisted", Version.parse("18.9.0")) assert package.name == "twisted" assert len(package.requires) == 71 assert sorted( (r for r in package.requires if not r.is_optional()), key=lambda r: r.name ) == [ Dependency("attrs", ">=17.4.0"), Dependency("Automat", ">=0.3.0"), Dependency("constantly", ">=15.1"), Dependency("hyperlink", ">=17.1.1"), Dependency("incremental", ">=16.10.1"), Dependency("PyHamcrest", ">=1.9.0"), Dependency("zope.interface", ">=4.4.2"), ] expected_extras = { "all-non-platform": [ Dependency("appdirs", ">=1.4.0"), Dependency("cryptography", ">=1.5"), Dependency("h2", ">=3.0,<4.0"), Dependency("idna", ">=0.6,!=2.3"), Dependency("priority", ">=1.1.0,<2.0"), Dependency("pyasn1", "*"), Dependency("pyopenssl", ">=16.0.0"), Dependency("pyserial", ">=3.0"), Dependency("service_identity", "*"), Dependency("soappy", "*"), ] } for name, expected_extra in expected_extras.items(): assert ( sorted(package.extras[canonicalize_name(name)], key=lambda r: r.name) == expected_extra ) def test_invalid_versions_ignored(pypi_repository: PyPiRepository) -> None: repo = pypi_repository # the json metadata for this package contains one malformed version # and a correct one. packages = repo.find_packages( Factory.create_dependency("invalid-version-package", "*") ) assert len(packages) == 1 @pytest.mark.usefixtures("pypi_repository") def test_get_should_invalid_cache_on_too_many_redirects_error( mocker: MockerFixture, ) -> None: delete_cache = mocker.patch("cachecontrol.caches.file_cache.FileCache.delete") response = Response() response.status_code = 200 response.encoding = "utf-8" response.raw = BytesIO(b'{"foo": "bar"}') mocker.patch( "poetry.utils.authenticator.Authenticator.get", side_effect=[TooManyRedirects(), response], ) repository = PyPiRepository() repository._get("https://pypi.org/pypi/async-timeout/json") assert delete_cache.called def test_urls(pypi_repository: PyPiRepository) -> None: repository = pypi_repository assert repository.url == "https://pypi.org/simple/" assert repository.authenticated_url == "https://pypi.org/simple/" def test_find_links_for_package_of_supported_types( pypi_repository: PyPiRepository, ) -> None: repo = pypi_repository package = repo.find_packages(Factory.create_dependency("hbmqtt", "0.9.6")) assert len(package) == 1 links = repo.find_links_for_package(package[0]) assert len(links) == 1 assert links[0].is_sdist assert links[0].show_url == "hbmqtt-0.9.6.tar.gz" def test_get_release_info_includes_only_supported_types( pypi_repository: PyPiRepository, ) -> None: repo = pypi_repository release_info = repo._get_release_info( name=canonicalize_name("hbmqtt"), version=Version.parse("0.9.6") ) assert len(release_info["files"]) == 1 assert release_info["files"][0]["file"] == "hbmqtt-0.9.6.tar.gz" @pytest.mark.parametrize( ("query", "count"), [ ("non-existent", 0), # no match ("requests", 6), # exact match ("hbmqtt==0.9.6", 1), # exact dependency match ("requests>=2.18.4", 2), # range dependency match ("request", 0), # partial match ("reques*", 0), # bad token ("reques t", 0), # bad token (["requests", "hbmqtt"], 7), # list of tokens ], ) def test_search_fallbacks_to_find_packages( query: str | list[str], count: int, pypi_repository: PyPiRepository, with_disallowed_pypi_search_html: None, ) -> None: repo = pypi_repository packages = repo.search(query) assert len(packages) == count ================================================ FILE: tests/repositories/test_repository.py ================================================ from __future__ import annotations import pytest from poetry.core.constraints.version import Version from poetry.factory import Factory from poetry.repositories import Repository from tests.helpers import get_package @pytest.fixture(scope="module") def repository() -> Repository: repo = Repository("repo") # latest version pre-release repo.add_package(get_package("foo", "1.0")) repo.add_package(get_package("foo", "2.0b0")) # latest version yanked repo.add_package(get_package("black", "19.10b0")) repo.add_package(get_package("black", "21.11b0", yanked="reason")) return repo @pytest.mark.parametrize( ("allow_prereleases", "constraint", "expected"), [ (None, ">=1.0", ["1.0"]), (False, ">=1.0", ["1.0"]), (True, ">=1.0", ["1.0", "2.0b0"]), (None, ">=1.5", ["2.0b0"]), (False, ">=1.5", []), (True, ">=1.5", ["2.0b0"]), ], ) def test_find_packages_allow_prereleases( repository: Repository, allow_prereleases: bool | None, constraint: str, expected: list[str], ) -> None: packages = repository.find_packages( Factory.create_dependency( "foo", {"version": constraint, "allow-prereleases": allow_prereleases} ) ) assert [str(p.version) for p in packages] == expected @pytest.mark.parametrize( ["constraint", "expected"], [ # yanked 21.11b0 is ignored except for pinned version ("*", ["19.10b0"]), (">=19.0a0", ["19.10b0"]), (">=20.0a0", []), (">=21.11b0", []), ("==21.11b0", ["21.11b0"]), ], ) def test_find_packages_yanked( repository: Repository, constraint: str, expected: list[str] ) -> None: packages = repository.find_packages(Factory.create_dependency("black", constraint)) assert [str(p.version) for p in packages] == expected @pytest.mark.parametrize( "package_name, version, yanked, yanked_reason", [ ("black", "19.10b0", False, ""), ("black", "21.11b0", True, "reason"), ], ) def test_package_yanked( repository: Repository, package_name: str, version: str, yanked: bool, yanked_reason: str, ) -> None: package = repository.package(package_name, Version.parse(version)) assert package.name == package_name assert str(package.version) == version assert package.yanked is yanked assert package.yanked_reason == yanked_reason def test_package_pretty_name_is_kept() -> None: pretty_name = "Not_canoni-calized.name" repo = Repository("repo") repo.add_package(get_package(pretty_name, "1.0")) package = repo.package(pretty_name, Version.parse("1.0")) assert package.pretty_name == pretty_name def test_search() -> None: package_foo1 = get_package("foo", "1.0.0") package_foo2 = get_package("foo", "2.0.0") package_foobar = get_package("foobar", "1.0.0") repo = Repository("repo", [package_foo1, package_foo2, package_foobar]) assert repo.search("foo") == [package_foo1, package_foo2, package_foobar] assert repo.search("bar") == [package_foobar] assert repo.search("nothing") == [] ================================================ FILE: tests/repositories/test_repository_pool.py ================================================ from __future__ import annotations import pytest from poetry.core.constraints.version import Version from poetry.repositories import Repository from poetry.repositories import RepositoryPool from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.repository_pool import Priority from tests.helpers import get_dependency from tests.helpers import get_package def test_pool() -> None: pool = RepositoryPool() assert len(pool.repositories) == 0 assert not pool.has_primary_repositories() def test_pool_with_initial_repositories() -> None: repo = Repository("repo") pool = RepositoryPool([repo]) assert len(pool.repositories) == 1 assert pool.has_primary_repositories() assert pool.get_priority("repo") == Priority.PRIMARY def test_repository_no_repository() -> None: pool = RepositoryPool() with pytest.raises(IndexError): pool.repository("foo") def test_adding_repositories_with_same_name_twice_raises_value_error() -> None: repo1 = Repository("repo") repo2 = Repository("repo") with pytest.raises(ValueError): RepositoryPool([repo1, repo2]) with pytest.raises(ValueError): RepositoryPool([repo1]).add_repository(repo2) @pytest.mark.parametrize("priority", (p for p in Priority)) def test_repository_from_single_repo_pool(priority: Priority) -> None: repo = LegacyRepository("foo", "https://foo.bar") pool = RepositoryPool() pool.add_repository(repo, priority=priority) assert pool.repository("foo") is repo assert pool.get_priority("foo") == priority def test_repository_with_all_prio_repositories() -> None: supplemental = LegacyRepository("supplemental", "https://supplemental.com") repo1 = LegacyRepository("foo", "https://foo.bar") repo2 = LegacyRepository("bar", "https://bar.baz") explicit = LegacyRepository("explicit", "https://bar.baz") pool = RepositoryPool() pool.add_repository(repo1) pool.add_repository(repo2) pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) pool.add_repository(explicit, priority=Priority.EXPLICIT) assert pool.repository("foo") is repo1 assert pool.repository("bar") is repo2 assert pool.repository("supplemental") is supplemental assert pool.repository("explicit") is explicit assert pool.has_primary_repositories() def test_repository_supplemental_repositories_do_show() -> None: supplemental = LegacyRepository("supplemental", "https://supplemental.com") pool = RepositoryPool() pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) assert pool.repository("supplemental") is supplemental assert pool.repositories == [supplemental] def test_repository_explicit_repositories_do_not_show() -> None: explicit = LegacyRepository("explicit", "https://explicit.com") primary = LegacyRepository("primary", "https://primary.com") pool = RepositoryPool() pool.add_repository(explicit, priority=Priority.EXPLICIT) pool.add_repository(primary, priority=Priority.PRIMARY) assert pool.repository("explicit") is explicit assert pool.repository("primary") is primary assert pool.repositories == [primary] assert pool.all_repositories == [primary, explicit] def test_remove_non_existing_repository_raises_indexerror() -> None: pool = RepositoryPool() with pytest.raises(IndexError): pool.remove_repository("foo") def test_remove_existing_repository_successful() -> None: repo1 = LegacyRepository("foo", "https://foo.bar") repo2 = LegacyRepository("bar", "https://bar.baz") repo3 = LegacyRepository("baz", "https://baz.quux") pool = RepositoryPool() pool.add_repository(repo1) pool.add_repository(repo2) pool.add_repository(repo3) pool.remove_repository("bar") assert pool.repository("foo") is repo1 assert not pool.has_repository("bar") assert pool.repository("baz") is repo3 def test_repository_ordering() -> None: primary1 = LegacyRepository("primary1", "https://primary1.com") primary2 = LegacyRepository("primary2", "https://primary2.com") primary3 = LegacyRepository("primary3", "https://primary3.com") supplemental = LegacyRepository("supplemental", "https://supplemental.com") pool = RepositoryPool() pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) pool.add_repository(primary1) pool.add_repository(primary2) pool.remove_repository("primary2") pool.add_repository(primary3) assert pool.repositories == [ primary1, primary3, supplemental, ] def test_pool_get_package_in_any_repository() -> None: package1 = get_package("foo", "1.0.0") repo1 = Repository("repo1", [package1]) package2 = get_package("bar", "1.0.0") repo2 = Repository("repo2", [package1, package2]) pool = RepositoryPool([repo1, repo2]) returned_package1 = pool.package("foo", Version.parse("1.0.0")) returned_package2 = pool.package("bar", Version.parse("1.0.0")) assert returned_package1 == package1 assert returned_package2 == package2 def test_pool_find_packages_only_considers_supplemental_when_needed() -> None: package1 = get_package("foo", "1.1.1") package2 = get_package("foo", "1.2.3") package3 = get_package("foo", "2.0.0") repo1 = Repository("repo1", [package1, package3]) repo2 = Repository("repo2", [package1, package2]) pool = RepositoryPool([repo1]).add_repository(repo2, priority=Priority.SUPPLEMENTAL) dependency_in_nonsupplemental = get_dependency("foo", "^1.0.0") returned_packages_in_nonsupplemental = pool.find_packages( dependency_in_nonsupplemental ) dependency_needs_supplemental = get_dependency("foo", "1.2.3") returned_packages_needs_supplemental = pool.find_packages( dependency_needs_supplemental ) assert returned_packages_in_nonsupplemental == [package1] assert returned_packages_needs_supplemental == [package2] def test_pool_get_package_in_specified_repository() -> None: package = get_package("foo", "1.0.0") repo1 = Repository("repo1", [package]) repo2 = Repository("repo2", [package]) pool = RepositoryPool([repo1]).add_repository(repo2, priority=Priority.SUPPLEMENTAL) returned_package = pool.package( "foo", Version.parse("1.0.0"), repository_name="repo2" ) assert returned_package == package def test_pool_no_package_from_any_repository_raises_package_not_found() -> None: pool = RepositoryPool() pool.add_repository(Repository("repo")) with pytest.raises(PackageNotFoundError): pool.package("foo", Version.parse("1.0.0")) def test_pool_no_package_from_specified_repository_raises_package_not_found() -> None: package = get_package("foo", "1.0.0") repo1 = Repository("repo1") repo2 = Repository("repo2", [package]) pool = RepositoryPool([repo1, repo2]) with pytest.raises(PackageNotFoundError): pool.package("foo", Version.parse("1.0.0"), repository_name="repo1") def test_pool_find_packages_in_any_repository() -> None: package1 = get_package("foo", "1.1.1") package2 = get_package("foo", "1.2.3") package3 = get_package("foo", "2.0.0") package4 = get_package("bar", "1.2.3") repo1 = Repository("repo1", [package1, package3]) repo2 = Repository("repo2", [package1, package2, package4]) pool = RepositoryPool([repo1, repo2]) available_dependency = get_dependency("foo", "^1.0.0") returned_packages_available = pool.find_packages(available_dependency) unavailable_dependency = get_dependency("foo", "999.9.9") returned_packages_unavailable = pool.find_packages(unavailable_dependency) assert returned_packages_available == [package1, package1, package2] assert returned_packages_unavailable == [] def test_pool_find_packages_in_specified_repository() -> None: package_foo1 = get_package("foo", "1.1.1") package_foo2 = get_package("foo", "1.2.3") package_foo3 = get_package("foo", "2.0.0") package_bar = get_package("bar", "1.2.3") repo1 = Repository("repo1", [package_foo1, package_foo3]) repo2 = Repository("repo2", [package_foo1, package_foo2, package_bar]) pool = RepositoryPool([repo1, repo2]) available_dependency = get_dependency("foo", "^1.0.0") available_dependency.source_name = "repo2" returned_packages_available = pool.find_packages(available_dependency) unavailable_dependency = get_dependency("foo", "999.9.9") unavailable_dependency.source_name = "repo2" returned_packages_unavailable = pool.find_packages(unavailable_dependency) assert returned_packages_available == [package_foo1, package_foo2] assert returned_packages_unavailable == [] def test_search_no_legacy_repositories() -> None: package_foo1 = get_package("foo", "1.0.0") package_foo2 = get_package("foo", "2.0.0") package_foobar = get_package("foobar", "1.0.0") repo1 = Repository("repo1", [package_foo1, package_foo2]) repo2 = Repository("repo2", [package_foo1, package_foobar]) pool = RepositoryPool([repo1, repo2]) assert pool.search("foo") == [ package_foo1, package_foo2, package_foo1, package_foobar, ] assert pool.search("bar") == [package_foobar] assert pool.search("nothing") == [] def test_search_legacy_repositories_are_not_skipped( legacy_repository: LegacyRepository, ) -> None: foo_package = get_package("foo", "1.0.0") demo_package = get_package("demo", "0.1.0") repo1 = Repository("repo1", [foo_package]) repo2 = legacy_repository pool = RepositoryPool([repo1, repo2]) assert pool.search("foo") == [foo_package] assert repo1.search("demo") == [] assert repo2.search("demo") == pool.search("demo") == [demo_package] ================================================ FILE: tests/repositories/test_single_page_repository.py ================================================ from __future__ import annotations import re from pathlib import Path from typing import TYPE_CHECKING from poetry.core.packages.dependency import Dependency from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.link_sources.html import HTMLPage from poetry.repositories.single_page_repository import SinglePageRepository if TYPE_CHECKING: from packaging.utils import NormalizedName class MockSinglePageRepository(SinglePageRepository): FIXTURES = Path(__file__).parent / "fixtures" / "single-page" def __init__(self, page: str) -> None: super().__init__( "single-page", url=f"http://single-page.foo.bar/single/page/repo/{page}.html", disable_cache=True, ) self._lazy_wheel = False def _get_page(self, name: NormalizedName) -> HTMLPage: fixture = self.FIXTURES / self.url.rsplit("/", 1)[-1] if not fixture.exists(): raise PackageNotFoundError(f"Package [{name}] not found.") with fixture.open(encoding="utf-8") as f: return HTMLPage(self._url, f.read()) def _download( self, url: str, dest: Path, *, raise_accepts_ranges: bool = False ) -> None: raise RuntimeError("Tests are not configured for downloads") def test_single_page_repository_get_page() -> None: repo = MockSinglePageRepository("jax_releases") page = repo.get_page("/ignored") links = list(page.links) assert len(links) == 21 for link in links: assert re.match(r"^(jax|jaxlib)-0\.3\.\d.*\.(whl|tar\.gz)$", link.filename) assert link.netloc == "storage.googleapis.com" assert link.path.startswith("/jax-releases/") def test_single_page_repository_find_packages() -> None: repo = MockSinglePageRepository("jax_releases") dep = Dependency("jaxlib", "0.3.7") packages = repo.find_packages(dep) assert len(packages) == 1 package = packages[0] assert package.name == dep.name assert package.to_dependency().to_pep_508() == dep.to_pep_508() def test_single_page_repository_get_page_with_relative_links() -> None: repo = MockSinglePageRepository("mmcv_torch_releases") base_path = Path("/single/page/torch1.12.0") page = repo.get_page("mmcv") for link in page.links: path = Path(link.path) assert path.parent == base_path ================================================ FILE: tests/test_conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from packaging.utils import canonicalize_name from poetry.factory import Factory from poetry.repositories import Repository if TYPE_CHECKING: from tests.types import PackageFactory @pytest.fixture def repo() -> Repository: return Repository("repo") def test_conftest_create_package( create_package: PackageFactory, repo: Repository ) -> None: dependency = Factory.create_dependency( "dependency", {"version": "*", "extras": ["download", "install"]} ) package = create_package( "A", "1.0", dependencies=[dependency], extras={ "download": ["download-package"], "install": ["install-package"], "py38": ["py38-package ; python_version == '3.8'"], "py310": ["py310-package ; python_version > '3.8'"], "all": ["a[download,install]"], "py": ["a[py38,py310]"], "nested": ["a[all]"], }, ) expected_extras = {"download", "install", "py38", "py310", "all", "py", "nested"} # test returned package instance assert package.name == "a" assert str(package.version) == "1.0" assert set(package.extras.keys()) == expected_extras # test package was correctly added to the repo assert repo.has_package(package) repo_package = repo.package(package.name, package.version) assert repo_package.name == "a" assert set(package.extras.keys()) == expected_extras assert repo.has_package(create_package("download-package", "1.0")) assert repo.has_package(create_package("install-package", "1.0")) assert repo.has_package(create_package("py38-package", "1.0")) assert repo.has_package(create_package("py310-package", "1.0")) # verify dependencies were correctly added requirements = {requirement.to_pep_508() for requirement in repo_package.requires} assert requirements == { dependency.to_pep_508(), 'download-package (>=1.0,<2.0) ; extra == "download"', 'install-package (>=1.0,<2.0) ; extra == "install"', 'py310-package (>=1.0,<2.0) ; python_version > "3.8" and extra == "py310"', 'py38-package (>=1.0,<2.0) ; python_version == "3.8" and extra == "py38"', } # verify self-referencing extras assert repo_package.extras[canonicalize_name("all")] == [ Factory.create_dependency( "a", {"version": "*", "extras": ["download", "install"]} ) ] assert repo_package.extras[canonicalize_name("nested")] == [ Factory.create_dependency("a", {"version": "*", "extras": ["all"]}) ] ================================================ FILE: tests/test_factory.py ================================================ from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING from typing import Any import pytest from cleo.io.buffered_io import BufferedIO from deepdiff.diff import DeepDiff from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.vcs_dependency import VCSDependency from poetry.__version__ import __version__ from poetry.exceptions import PoetryError from poetry.factory import Factory from poetry.plugins.plugin import Plugin from poetry.repositories.exceptions import InvalidSourceError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.repository_pool import Priority from poetry.toml.file import TOMLFile from tests.helpers import mock_metadata_entry_points if TYPE_CHECKING: from cleo.io.io import IO from pytest_mock import MockerFixture from poetry.config.config import Config from poetry.poetry import Poetry from tests.types import FixtureDirGetter class MyPlugin(Plugin): def activate(self, poetry: Poetry, io: IO) -> None: io.write_line("Setting readmes") poetry.package.readmes = (Path("README.md"),) def test_create_poetry(fixture_dir: FixtureDirGetter) -> None: poetry = Factory().create_poetry(fixture_dir("sample_project")) package = poetry.package assert package.name == "sample-project" assert package.version.text == "1.2.3" assert package.description == "Some description." assert package.authors == ["Sébastien Eustace "] assert package.license is not None assert package.license.id == "MIT" for readme in package.readmes: assert ( readme.relative_to(fixture_dir("sample_project")).as_posix() == "README.rst" ) assert package.homepage == "https://python-poetry.org" assert package.repository_url == "https://github.com/python-poetry/poetry" assert package.keywords == ["packaging", "dependency", "poetry"] assert package.python_versions == "~2.7 || ^3.6" assert str(package.python_constraint) == ">=2.7,<2.8 || >=3.6,<4.0" dependencies = {} for dep in package.requires: dependencies[dep.name] = dep cleo = dependencies[canonicalize_name("cleo")] assert cleo.pretty_constraint == "^0.6" assert not cleo.is_optional() pendulum = dependencies[canonicalize_name("pendulum")] assert pendulum.pretty_constraint == "branch 2.0" assert pendulum.is_vcs() assert isinstance(pendulum, VCSDependency) assert pendulum.vcs == "git" assert pendulum.branch == "2.0" assert pendulum.source == "https://github.com/sdispater/pendulum.git" assert pendulum.allows_prereleases() requests = dependencies[canonicalize_name("requests")] assert requests.pretty_constraint == "^2.18" assert not requests.is_vcs() assert not requests.allows_prereleases() assert requests.is_optional() assert requests.extras == frozenset(["security"]) pathlib2 = dependencies[canonicalize_name("pathlib2")] assert pathlib2.pretty_constraint == "^2.2" assert parse_constraint(pathlib2.python_versions) == parse_constraint("~2.7") assert not pathlib2.is_optional() demo = dependencies[canonicalize_name("demo")] assert demo.is_file() assert not demo.is_vcs() assert demo.name == "demo" assert demo.pretty_constraint == "*" demo = dependencies[canonicalize_name("my-package")] assert not demo.is_file() assert demo.is_directory() assert not demo.is_vcs() assert demo.name == "my-package" assert demo.pretty_constraint == "*" simple_project = dependencies[canonicalize_name("simple-project")] assert not simple_project.is_file() assert simple_project.is_directory() assert not simple_project.is_vcs() assert simple_project.name == "simple-project" assert simple_project.pretty_constraint == "*" functools32 = dependencies[canonicalize_name("functools32")] assert functools32.name == "functools32" assert functools32.pretty_constraint == "^3.2.3" assert ( str(functools32.marker) == 'python_version ~= "2.7" and sys_platform == "win32" or python_version in' ' "3.4 3.5"' ) assert "db" in package.extras classifiers = package.classifiers assert classifiers == [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] assert package.all_classifiers == [ "License :: OSI Approved :: MIT License", *( f"Programming Language :: Python :: {version}" for version in sorted( Package.AVAILABLE_PYTHONS, key=lambda x: tuple(map(int, x.split("."))), ) if package.python_constraint.allows_any( parse_constraint(version + ".*") if len(version) == 1 else Version.parse(version) ) ), "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] @pytest.mark.parametrize( ("project",), [ ("simple_project_legacy",), ("project_with_extras",), ], ) def test_create_pyproject_from_package( project: str, fixture_dir: FixtureDirGetter ) -> None: poetry = Factory().create_poetry(fixture_dir(project)) package = poetry.package pyproject: dict[str, Any] = Factory.create_legacy_pyproject_from_package(package) result = pyproject["tool"]["poetry"] expected = poetry.pyproject.poetry_config # Extras are normalized as they are read. extras = expected.pop("extras", None) if extras is not None: normalized_extras = { canonicalize_name(extra): dependencies for extra, dependencies in extras.items() } expected["extras"] = normalized_extras # packages do not support this at present expected.pop("scripts", None) # remove any empty sections sections = list(expected.keys()) for section in sections: if not expected[section]: expected.pop(section) assert not DeepDiff(expected, result) def test_create_poetry_with_packages_and_includes( fixture_dir: FixtureDirGetter, ) -> None: poetry = Factory().create_poetry(fixture_dir("with-include")) package = poetry.package assert package.packages == [ {"include": "extra_dir/**/*.py", "format": ["sdist", "wheel"]}, {"include": "extra_dir/**/*.py", "format": ["sdist", "wheel"]}, {"include": "my_module.py", "format": ["sdist", "wheel"]}, {"include": "package_with_include", "format": ["sdist", "wheel"]}, {"include": "tests", "format": ["sdist"]}, {"include": "for_wheel_only", "format": ["wheel"]}, {"include": "src_package", "from": "src", "format": ["sdist", "wheel"]}, ] assert package.include in ( # with https://github.com/python-poetry/poetry-core/pull/773 [ {"path": "extra_dir/vcs_excluded.txt", "format": ["sdist", "wheel"]}, {"path": "notes.txt", "format": ["sdist"]}, ], # without https://github.com/python-poetry/poetry-core/pull/773 [ {"path": "extra_dir/vcs_excluded.txt", "format": ["sdist"]}, {"path": "notes.txt", "format": ["sdist"]}, ], ) def test_create_poetry_with_multi_constraints_dependency( fixture_dir: FixtureDirGetter, ) -> None: poetry = Factory().create_poetry( fixture_dir("project_with_multi_constraints_dependency") ) package = poetry.package assert len(package.requires) == 2 def test_create_poetry_non_package_mode(fixture_dir: FixtureDirGetter) -> None: poetry = Factory().create_poetry(fixture_dir("non_package_mode")) assert not poetry.is_package_mode def test_create_poetry_version_ok(fixture_dir: FixtureDirGetter) -> None: io = BufferedIO() Factory().create_poetry(fixture_dir("self_version_ok"), io=io) assert io.fetch_output() == "" assert io.fetch_error() == "" def test_create_poetry_version_not_ok(fixture_dir: FixtureDirGetter) -> None: with pytest.raises(PoetryError) as e: Factory().create_poetry(fixture_dir("self_version_not_ok")) assert ( str(e.value) == f"This project requires Poetry <1.2, but you are using Poetry {__version__}" ) def test_create_poetry_check_version_before_validation( fixture_dir: FixtureDirGetter, ) -> None: with pytest.raises(PoetryError) as e: Factory().create_poetry(fixture_dir("self_version_not_ok_invalid_config")) assert ( str(e.value) == f"This project requires Poetry <1.2, but you are using Poetry {__version__}" ) @pytest.mark.parametrize( "project", ("with_primary_source_implicit", "with_primary_source_explicit"), ) def test_poetry_with_primary_source( project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() poetry = Factory().create_poetry(fixture_dir(project), io=io) assert not poetry.pool.has_repository("PyPI") assert poetry.pool.has_repository("foo") assert poetry.pool.get_priority("foo") is Priority.PRIMARY assert isinstance(poetry.pool.repository("foo"), LegacyRepository) assert {repo.name for repo in poetry.pool.repositories} == {"foo"} def test_poetry_with_multiple_supplemental_sources( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: poetry = Factory().create_poetry(fixture_dir("with_multiple_supplemental_sources")) assert poetry.pool.has_repository("PyPI") assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY assert poetry.pool.has_repository("foo") assert isinstance(poetry.pool.repository("foo"), LegacyRepository) assert poetry.pool.has_repository("bar") assert isinstance(poetry.pool.repository("bar"), LegacyRepository) assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo", "bar"} def test_poetry_with_multiple_sources( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: poetry = Factory().create_poetry(fixture_dir("with_multiple_sources")) assert not poetry.pool.has_repository("PyPI") assert poetry.pool.has_repository("bar") assert isinstance(poetry.pool.repository("bar"), LegacyRepository) assert poetry.pool.has_repository("foo") assert isinstance(poetry.pool.repository("foo"), LegacyRepository) assert {repo.name for repo in poetry.pool.repositories} == {"bar", "foo"} def test_poetry_with_multiple_sources_pypi( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() poetry = Factory().create_poetry(fixture_dir("with_multiple_sources_pypi"), io=io) assert len(poetry.pool.repositories) == 4 assert poetry.pool.has_repository("PyPI") assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY # PyPI must be between bar and baz! expected = ["bar", "PyPI", "baz", "foo"] assert [repo.name for repo in poetry.pool.repositories] == expected def test_poetry_with_no_default_source(fixture_dir: FixtureDirGetter) -> None: poetry = Factory().create_poetry(fixture_dir("sample_project")) assert poetry.pool.has_repository("PyPI") assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) assert {repo.name for repo in poetry.pool.repositories} == {"PyPI"} def test_poetry_with_supplemental_source( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() poetry = Factory().create_poetry(fixture_dir("with_supplemental_source"), io=io) assert poetry.pool.has_repository("PyPI") assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) assert poetry.pool.has_repository("supplemental") assert poetry.pool.get_priority("supplemental") is Priority.SUPPLEMENTAL assert isinstance(poetry.pool.repository("supplemental"), LegacyRepository) assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "supplemental"} assert io.fetch_error() == "" def test_poetry_with_explicit_source( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() poetry = Factory().create_poetry(fixture_dir("with_explicit_source"), io=io) assert len(poetry.pool.repositories) == 1 assert len(poetry.pool.all_repositories) == 2 assert poetry.pool.has_repository("PyPI") assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) assert poetry.pool.has_repository("explicit") assert isinstance(poetry.pool.repository("explicit"), LegacyRepository) assert {repo.name for repo in poetry.pool.repositories} == {"PyPI"} assert io.fetch_error() == "" def test_poetry_with_explicit_pypi_and_other( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() poetry = Factory().create_poetry(fixture_dir("with_explicit_pypi_and_other"), io=io) assert len(poetry.pool.repositories) == 1 assert len(poetry.pool.all_repositories) == 2 error = io.fetch_error() assert error == "" @pytest.mark.parametrize( "project", ["with_explicit_pypi_no_other", "with_explicit_pypi_and_other_explicit"] ) def test_poetry_with_pypi_explicit_only( project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: with pytest.raises(PoetryError) as e: Factory().create_poetry(fixture_dir(project)) assert str(e.value) == "At least one source must not be configured as 'explicit'." def test_poetry_with_build_constraints(fixture_dir: FixtureDirGetter) -> None: poetry = Factory().create_poetry(fixture_dir("build_constraints")) assert set(poetry.build_constraints) == { "legacy-lib", "no-constraints", "c-ext-lib", } assert poetry.build_constraints[canonicalize_name("legacy-lib")] == [ Dependency("setuptools", "<75") ] assert poetry.build_constraints[canonicalize_name("no-constraints")] == [] assert poetry.build_constraints[canonicalize_name("c-ext-lib")] == [ Dependency("Cython", "<3.1"), Dependency("setuptools", ">=60,<75"), Dependency("setuptools", ">=75"), ] def test_poetry_with_empty_build_constraints(fixture_dir: FixtureDirGetter) -> None: poetry = Factory().create_poetry(fixture_dir("build_constraints_empty")) assert set(poetry.build_constraints) == set() def test_validate(fixture_dir: FixtureDirGetter) -> None: complete = TOMLFile(fixture_dir("complete.toml")) pyproject: dict[str, Any] = complete.read() assert Factory.validate(pyproject) == {"errors": [], "warnings": []} def test_validate_fails(fixture_dir: FixtureDirGetter) -> None: complete = TOMLFile(fixture_dir("complete.toml")) pyproject: dict[str, Any] = complete.read() pyproject["tool"]["poetry"]["this key is not in the schema"] = "" pyproject["tool"]["poetry"]["source"] = {} expected = [ "tool.poetry.source must be array", ( "Additional properties are not allowed " "('this key is not in the schema' was unexpected)" ), ] assert Factory.validate(pyproject) == {"errors": expected, "warnings": []} def test_create_poetry_fails_on_invalid_configuration( fixture_dir: FixtureDirGetter, ) -> None: with pytest.raises(RuntimeError) as e: Factory().create_poetry(fixture_dir("invalid_pyproject_dep_name")) expected = """\ The Poetry configuration is invalid: - Project name (invalid) is same as one of its dependencies """ assert str(e.value) == expected def test_create_poetry_fails_on_nameless_project( fixture_dir: FixtureDirGetter, ) -> None: with pytest.raises(RuntimeError) as e: Factory().create_poetry(fixture_dir("nameless_pyproject")) expected = """\ The Poetry configuration is invalid: - Either [project.name] or [tool.poetry.name] is required in package mode. """ assert str(e.value) == expected def test_create_poetry_with_local_config(fixture_dir: FixtureDirGetter) -> None: poetry = Factory().create_poetry(fixture_dir("with_local_config")) assert not poetry.config.get("virtualenvs.in-project") assert not poetry.config.get("virtualenvs.create") assert not poetry.config.get("virtualenvs.options.always-copy") assert not poetry.config.get("virtualenvs.options.no-pip") assert not poetry.config.get("virtualenvs.options.system-site-packages") def test_create_poetry_with_plugins( mocker: MockerFixture, fixture_dir: FixtureDirGetter ) -> None: mock_metadata_entry_points(mocker, MyPlugin) poetry = Factory().create_poetry(fixture_dir("sample_project")) assert poetry.package.readmes == (Path("README.md"),) @pytest.mark.parametrize( ("source", "expected"), [ ({}, "Missing [name] in source."), ({"name": "foo"}, "Missing [url] in source 'foo'."), ( {"name": "PyPI", "url": "https://example.com"}, "The PyPI repository cannot be configured with a custom url.", ), ], ) def test_create_package_source_invalid( source: dict[str, str], expected: str, config: Config, fixture_dir: FixtureDirGetter, ) -> None: with pytest.raises(InvalidSourceError) as e: Factory.create_package_source(source, config=config) Factory().create_poetry(fixture_dir("with_source_pypi_url")) assert str(e.value) == expected ================================================ FILE: tests/test_helpers.py ================================================ from __future__ import annotations import os from tests.helpers import flatten_dict from tests.helpers import isolated_environment def test_flatten_dict() -> None: orig_dict = { "a": 1, "b": 2, "c": { "x": 8, "y": 9, }, } flattened_dict = { "a": 1, "b": 2, "c:x": 8, "c:y": 9, } assert flattened_dict == flatten_dict(orig_dict, delimiter=":") def test_isolated_environment_restores_original_environ() -> None: original_environ = dict(os.environ) with isolated_environment(): os.environ["TEST_VAR"] = "test" assert os.environ == original_environ def test_isolated_environment_clears_environ() -> None: os.environ["TEST_VAR"] = "test" with isolated_environment(clear=True): assert "TEST_VAR" not in os.environ assert "TEST_VAR" in os.environ def test_isolated_environment_updates_environ() -> None: with isolated_environment(environ={"NEW_VAR": "new_value"}): assert os.environ["NEW_VAR"] == "new_value" assert "NEW_VAR" not in os.environ ================================================ FILE: tests/types.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import Protocol if TYPE_CHECKING: from collections.abc import Callable from contextlib import AbstractContextManager from pathlib import Path from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from cleo.io.io import IO from cleo.testers.command_tester import CommandTester from packaging.utils import NormalizedName from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from requests import PreparedRequest from poetry.config.config import Config from poetry.config.source import Source from poetry.console.commands.command import Command from poetry.installation import Installer from poetry.installation.executor import Executor from poetry.poetry import Poetry from poetry.repositories.legacy_repository import LegacyRepository from poetry.utils.env import Env from poetry.utils.env.python import Python from tests.repositories.fixtures.distribution_hashes import DistributionHash HttpResponse = tuple[int, dict[str, str], bytes | str] # status code, headers, body HttpRequestCallback = Callable[[PreparedRequest], HttpResponse] HttpRequestCallbackWrapper = Callable[[HttpRequestCallback], HttpRequestCallback] class CommandTesterFactory(Protocol): def __call__( self, command: str, poetry: Poetry | None = None, installer: Installer | None = None, executor: Executor | None = None, environment: Env | None = None, ) -> CommandTester: ... class SourcesFactory(Protocol): def __call__( self, poetry: Poetry, sources: Source, config: Config, io: IO ) -> None: ... class ProjectFactory(Protocol): def __call__( self, name: str | None = None, dependencies: dict[str, str] | None = None, dev_dependencies: dict[str, str] | None = None, pyproject_content: str | None = None, poetry_lock_content: str | None = None, install_deps: bool = True, source: Path | None = None, locker_config: dict[str, Any] | None = None, use_test_locker: bool = True, ) -> Poetry: ... class PackageFactory(Protocol): def __call__( self, name: str, version: str | None = None, dependencies: list[Dependency] | None = None, extras: dict[str, list[str]] | None = None, merge_extras: bool = False, ) -> Package: ... class CommandFactory(Protocol): def __call__( self, command_name: str, command_arguments: list[Argument] | None = None, command_options: list[Option] | None = None, command_description: str = "", command_help: str = "", command_handler: Callable[[Command], int] | str | None = None, ) -> Command: ... class FixtureDirGetter(Protocol): def __call__(self, name: str) -> Path: ... class FixtureCopier(Protocol): def __call__(self, relative_path: str, target: Path | None = None) -> Path: ... class HTMLPageGetter(Protocol): def __call__(self, content: str, base_url: str | None = None) -> str: ... class NormalizedNameTransformer(Protocol): def __call__(self, name: str) -> NormalizedName: ... class SpecializedLegacyRepositoryMocker(Protocol): def __call__( self, transformer_or_suffix: NormalizedNameTransformer | str, repository_name: str = "special", repository_url: str = "https://legacy.foo.bar", ) -> LegacyRepository: ... class PythonHostedFileMocker(Protocol): def __call__( self, distribution_locations: list[Path], metadata_locations: list[Path], ) -> None: ... class PackageDistributionLookup(Protocol): def __call__(self, name: str) -> Path | None: ... class DistributionHashGetter(Protocol): def __call__(self, name: str) -> DistributionHash: ... class SetProjectContext(Protocol): def __call__( self, project: str | Path, in_place: bool = False ) -> AbstractContextManager[Path]: ... class MockedPythonRegister(Protocol): def __call__( self, version: str, executable_name: str | Path | None = None, implementation: str | None = None, free_threaded: bool = False, parent: str | Path | None = None, make_system: bool = False, ) -> Python: ... class MockedPoetryPythonRegister(Protocol): def __call__( self, version: str, implementation: str, free_threaded: bool = False, with_install_dir: bool = False, ) -> Path: ... ================================================ FILE: tests/utils/__init__.py ================================================ ================================================ FILE: tests/utils/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from poetry.poetry import Poetry from poetry.utils.env import EnvManager @pytest.fixture def venv_name( manager: EnvManager, poetry: Poetry, ) -> str: return manager.generate_env_name( poetry.package.name, str(poetry.file.path.parent), ) ================================================ FILE: tests/utils/env/__init__.py ================================================ ================================================ FILE: tests/utils/env/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.utils.env import EnvManager if TYPE_CHECKING: from poetry.poetry import Poetry from tests.types import FixtureDirGetter from tests.types import ProjectFactory @pytest.fixture def poetry(project_factory: ProjectFactory, fixture_dir: FixtureDirGetter) -> Poetry: return project_factory("simple", source=fixture_dir("simple_project")) @pytest.fixture def manager(poetry: Poetry) -> EnvManager: return EnvManager(poetry) ================================================ FILE: tests/utils/env/python/__init__.py ================================================ ================================================ FILE: tests/utils/env/python/test_manager.py ================================================ from __future__ import annotations import platform import sys from typing import TYPE_CHECKING import pytest from poetry.core.constraints.version import Version from poetry.core.constraints.version import parse_constraint from poetry.utils.env.python import Python from tests.helpers import pbs_installer_supported_arch if TYPE_CHECKING: from tests.types import MockedPoetryPythonRegister from tests.types import MockedPythonRegister def test_find_all(without_mocked_findpython: None) -> None: assert len(list(Python.find_all())) > 1 def test_find_all_with_poetry_managed( without_mocked_findpython: None, mocked_poetry_managed_python_register: MockedPoetryPythonRegister, ) -> None: cpython_path = mocked_poetry_managed_python_register("3.9.1", "cpython") pypy_path = mocked_poetry_managed_python_register("3.10.8", "pypy") found_pythons = list(Python.find_all()) assert len(found_pythons) > 3 for poetry_python in (cpython_path, pypy_path): assert any(p.executable.parent == poetry_python for p in found_pythons) def test_find_poetry_managed_pythons_none() -> None: assert list(Python.find_poetry_managed_pythons()) == [] def test_find_poetry_managed_pythons( mocked_poetry_managed_python_register: MockedPoetryPythonRegister, ) -> None: mocked_poetry_managed_python_register("3.9.1", "cpython") mocked_poetry_managed_python_register("3.10.8", "pypy") assert len(list(Python.find_poetry_managed_pythons())) == 3 @pytest.mark.parametrize( ("constraint", "implementation", "free_threaded", "expected"), [ (None, None, None, 5), (None, "CPython", None, 4), (None, "cpython", None, 4), (None, "pypy", None, 1), ("~3.9", None, None, 2), ("~3.9", "cpython", None, 2), ("~3.9", "pypy", None, 0), (">=3.9.2", None, None, 4), (">=3.9.2", "cpython", None, 3), (">=3.9.2", "pypy", None, 1), (">=3.10", None, None, 3), (">=3.10", None, False, 2), (">=3.10", None, True, 1), ("~3.11", None, None, 0), ], ) def test_find_all_versions( mocked_python_register: MockedPythonRegister, constraint: str | None, implementation: str | None, free_threaded: bool | None, expected: int, ) -> None: mocked_python_register("3.9.1", implementation="CPython", parent="a") mocked_python_register("3.9.3", implementation="CPython", parent="b") mocked_python_register("3.10.4", implementation="PyPy", parent="c") mocked_python_register("3.14.0", implementation="CPython", parent="d") mocked_python_register( "3.14.0", implementation="CPython", free_threaded=True, parent="e" ) versions = list(Python.find_all_versions(constraint, implementation, free_threaded)) assert len(versions) == expected @pytest.mark.parametrize("constraint", [None, "~3.9", ">=3.10"]) def test_find_downloadable_versions(constraint: str | None) -> None: versions = list(Python.find_downloadable_versions(constraint)) if platform.system() == "FreeBSD" or not pbs_installer_supported_arch( platform.machine() ): assert len(versions) == 0 else: assert len(versions) > 0 if constraint: parsed_constraint = parse_constraint(constraint) assert all( parsed_constraint.allows( Version.parse(f"{v.major}.{v.minor}.{v.patch}") ) for v in versions ) else: assert len({v.free_threaded for v in versions}) == 2 assert len({v.implementation for v in versions}) >= 2 def find_downloadable_versions_include_incompatible() -> None: assert len( list(Python.find_downloadable_versions(include_incompatible=True)) ) > len(list(Python.find_downloadable_versions())) @pytest.mark.parametrize( ("name", "expected_minor"), [ ("3.9", 9), ("3.10", 10), ("3.11", None), ], ) def test_get_by_name_version( mocked_python_register: MockedPythonRegister, name: str, expected_minor: int | None ) -> None: mocked_python_register("3.9.1", implementation="CPython", parent="a") mocked_python_register("3.10.3", implementation="CPython", parent="b") python = Python.get_by_name(name) if expected_minor is None: assert python is None else: assert python is not None assert python.minor == expected_minor def test_get_by_name_python(without_mocked_findpython: None) -> None: python = Python.get_by_name("python") assert python is not None assert python.version.major == 3 assert python.version.minor == sys.version_info.minor def test_get_by_name_path(without_mocked_findpython: None) -> None: python = Python.get_by_name(sys.executable) assert python is not None assert python.version.major == 3 assert python.version.minor == sys.version_info.minor ================================================ FILE: tests/utils/env/python/test_python_installer.py ================================================ from __future__ import annotations from subprocess import CalledProcessError from typing import TYPE_CHECKING from typing import Literal from typing import cast import pytest from poetry.core.constraints.version import Version from poetry.console.exceptions import PoetryRuntimeError from poetry.utils.env.python.installer import PythonDownloadNotFoundError from poetry.utils.env.python.installer import PythonInstallationError from poetry.utils.env.python.installer import PythonInstaller if TYPE_CHECKING: from pathlib import Path from unittest.mock import MagicMock from pytest_mock import MockerFixture @pytest.fixture(autouse=True) def mock_get_download_link(mocker: MockerFixture) -> MagicMock: return mocker.patch( "pbs_installer.get_download_link", return_value=(mocker.Mock(major=3, minor=9, micro=1), None), ) def test_python_installer_version() -> None: installer = PythonInstaller(request="3.9.1") assert installer.version == Version.from_parts(3, 9, 1) def test_python_installer_version_not_found(mock_get_download_link: MagicMock) -> None: mock_get_download_link.return_value = [] installer = PythonInstaller(request="3.9.1") with pytest.raises(PythonDownloadNotFoundError): _ = installer.version def test_python_installer_exists(mocker: MockerFixture) -> None: mocker.patch( "poetry.utils.env.python.Python.find_poetry_managed_pythons", return_value=[ mocker.Mock( implementation="cpython", free_threaded=False, version=Version.from_parts(3, 9, 1), ) ], ) installer = PythonInstaller(request="3.9.1") assert installer.exists() def test_python_installer_does_not_exist(mocker: MockerFixture) -> None: mocker.patch( "poetry.utils.env.python.Python.find_poetry_managed_pythons", return_value=[] ) installer = PythonInstaller(request="3.9.1") assert not installer.exists() def test_python_installer_exists_with_bad_executables(mocker: MockerFixture) -> None: class BadPython: @property def implementation(self) -> str: return "cpython" @property def free_threaded(self) -> bool: return False @property def executable(self) -> Path: return cast("Path", mocker.Mock(as_posix=lambda: "/path/to/bad/python")) @property def version(self) -> None: raise CalledProcessError(1, "cmd") mocker.patch( "poetry.utils.env.python.Python.find_poetry_managed_pythons", return_value=[BadPython()], ) installer = PythonInstaller(request="3.9.1") with pytest.raises(PoetryRuntimeError): assert not installer.exists() @pytest.mark.parametrize( ("implementation", "requested", "expected"), [ ("cpython", "pypy", False), ("pypy", "pypy", True), ], ) def test_python_installer_exists_implementation( mocker: MockerFixture, implementation: Literal["cpython", "pypy"], requested: Literal["cpython", "pypy"], expected: bool, ) -> None: mocker.patch( "poetry.utils.env.python.Python.find_poetry_managed_pythons", return_value=[ mocker.Mock( implementation=implementation, free_threaded=False, version=Version.from_parts(3, 9, 1), ) ], ) installer = PythonInstaller(request="3.9.1", implementation=requested) assert installer.exists() is expected @pytest.mark.parametrize( ("free_threaded", "requested", "expected"), [ ("cpython", "pypy", False), ("pypy", "pypy", True), ], ) def test_python_installer_exists_free_threaded( mocker: MockerFixture, free_threaded: bool, requested: bool, expected: bool ) -> None: mocker.patch( "poetry.utils.env.python.Python.find_poetry_managed_pythons", return_value=[ mocker.Mock( implementation="cpython", free_threaded=free_threaded, version=Version.from_parts(3, 9, 1), ) ], ) installer = PythonInstaller(request="3.9.1", free_threaded=requested) assert installer.exists() is expected def test_python_installer_install(mocker: MockerFixture) -> None: mocker.patch( "pbs_installer.get_download_link", return_value=(mocker.Mock(major=3, minor=9, micro=1), None), ) install = mocker.patch("pbs_installer.install") installer = PythonInstaller(request="3.9.1") installer.install() install.assert_called_once_with( "3.9.1", installer.installation_directory, version_dir=True, implementation="cpython", free_threaded=False, ) def test_python_installer_install_error(mocker: MockerFixture) -> None: mocker.patch("pbs_installer.install", side_effect=ValueError) installer = PythonInstaller(request="3.9.1") with pytest.raises(PythonInstallationError): installer.install() ================================================ FILE: tests/utils/env/python/test_python_providers.py ================================================ from __future__ import annotations import sys from typing import TYPE_CHECKING from poetry.core.constraints.version import Version from poetry.utils.env.python.providers import PoetryPythonPathProvider from poetry.utils.env.python.providers import ShutilWhichPythonProvider if TYPE_CHECKING: from tests.types import MockedPoetryPythonRegister def test_shutil_which_python_provider() -> None: provider = ShutilWhichPythonProvider.create() assert provider pythons = list(provider.find_pythons()) assert len(pythons) == 1 assert pythons[0].minor == sys.version_info.minor def test_poetry_python_path_provider_no_pythons() -> None: provider = PoetryPythonPathProvider.create() assert provider assert not provider.paths def test_poetry_python_path_provider( mocked_poetry_managed_python_register: MockedPoetryPythonRegister, ) -> None: cpython_path = mocked_poetry_managed_python_register("3.9.1", "cpython") pypy_path = mocked_poetry_managed_python_register("3.10.8", "pypy") free_threaded_path = mocked_poetry_managed_python_register( "3.13.2", "cpython", free_threaded=True, with_install_dir=True ) provider = PoetryPythonPathProvider.create() assert provider assert set(provider.paths) == {cpython_path, pypy_path, free_threaded_path} assert len(list(provider.find_pythons())) == 4 assert provider.installation_bin_paths(Version.parse("3.9.1"), "cpython") == [ cpython_path ] assert provider.installation_bin_paths(Version.parse("3.9.2"), "cpython") == [] assert provider.installation_bin_paths(Version.parse("3.10.8"), "pypy") == [ pypy_path ] assert provider.installation_bin_paths(Version.parse("3.10.8"), "cpython") == [] assert provider.installation_bin_paths( Version.parse("3.13.2"), "cpython", free_threaded=True ) == [free_threaded_path] ================================================ FILE: tests/utils/env/test_env.py ================================================ from __future__ import annotations import os import re import subprocess import sys from importlib import metadata from pathlib import Path from threading import Thread from typing import TYPE_CHECKING import packaging.tags import pytest from deepdiff.diff import DeepDiff from installer.utils import SCHEME_NAMES from poetry.factory import Factory from poetry.repositories.installed_repository import InstalledRepository from poetry.utils._compat import WINDOWS from poetry.utils.env import EnvCommandError from poetry.utils.env import EnvManager from poetry.utils.env import GenericEnv from poetry.utils.env import MockEnv from poetry.utils.env import SystemEnv from poetry.utils.env import VirtualEnv from poetry.utils.env import build_environment from poetry.utils.env import ephemeral_environment from poetry.utils.helpers import is_dir_writable if TYPE_CHECKING: from collections.abc import Iterator from pytest_mock import MockerFixture from poetry.poetry import Poetry from tests.types import FixtureDirGetter from tests.types import SetProjectContext MINIMAL_SCRIPT = """\ print("Minimal Output"), """ # Script expected to fail. ERRORING_SCRIPT = """\ import nullpackage print("nullpackage loaded"), """ class MockVirtualEnv(VirtualEnv): def __init__( self, path: Path, base: Path | None = None, sys_path: list[str] | None = None, ) -> None: super().__init__(path, base=base) self._sys_path = sys_path @property def sys_path(self) -> list[str]: if self._sys_path is not None: return self._sys_path return super().sys_path def test_virtualenvs_with_spaces_in_their_path_work_as_expected( tmp_path: Path, manager: EnvManager ) -> None: venv_path = tmp_path / "Virtual Env" manager.build_venv(venv_path) venv = VirtualEnv(venv_path) assert venv.run("python", "-V").startswith("Python") def test_env_commands_with_spaces_in_their_arg_work_as_expected( tmp_path: Path, manager: EnvManager ) -> None: venv_path = tmp_path / "Virtual Env" manager.build_venv(venv_path) venv = VirtualEnv(venv_path) output = venv.run("python", str(venv.pip), "--version") assert re.match(r"pip \S+ from", output) @pytest.mark.parametrize("differing_platform", [True, False]) def test_env_get_supported_tags_matches_inside_virtualenv( tmp_path: Path, manager: EnvManager, mocker: MockerFixture, differing_platform: bool ) -> None: venv_path = tmp_path / "Virtual Env" manager.build_venv(venv_path) venv = VirtualEnv(venv_path) run_python_script_spy = mocker.spy(venv, "run_python_script") # determine expected tags before patching sysconfig! expected_tags = list(packaging.tags.sys_tags()) if differing_platform: mocker.patch("sysconfig.get_platform", return_value="some_other_platform") expected_call_count = 2 else: expected_call_count = 1 assert venv.get_supported_tags() == expected_tags assert run_python_script_spy.call_count == expected_call_count @pytest.mark.skipif( sys.implementation.name != "cpython", reason="free threading is only relevant for CPython", ) def test_env_get_supported_tags_free_threading( tmp_path: Path, manager: EnvManager ) -> None: venv_path = tmp_path / "Virtual Env" manager.build_venv(venv_path) venv = VirtualEnv(venv_path) if venv.marker_env["free_threading"]: assert venv.get_supported_tags() == list(packaging.tags.sys_tags()) else: assert not any(t.abi.endswith("t") for t in venv.get_supported_tags()) venv.marker_env["free_threading"] = True assert any(t.abi.endswith("t") for t in venv.get_supported_tags()) @pytest.mark.skipif(os.name == "nt", reason="Symlinks are not support for Windows") def test_env_has_symlinks_on_nix(tmp_path: Path, tmp_venv: VirtualEnv) -> None: assert os.path.islink(tmp_venv.python) def test_run_with_keyboard_interrupt( tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture ) -> None: mocker.patch("subprocess.check_output", side_effect=KeyboardInterrupt()) with pytest.raises(KeyboardInterrupt): tmp_venv.run("python", "-c", MINIMAL_SCRIPT) subprocess.check_output.assert_called_once() # type: ignore[attr-defined] def test_call_with_keyboard_interrupt( tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture ) -> None: mocker.patch("subprocess.check_call", side_effect=KeyboardInterrupt()) kwargs = {"call": True} with pytest.raises(KeyboardInterrupt): tmp_venv.run("python", "-", **kwargs) subprocess.check_call.assert_called_once() # type: ignore[attr-defined] def test_run_with_called_process_error( tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture ) -> None: mocker.patch( "subprocess.check_output", side_effect=subprocess.CalledProcessError( 42, "some_command", "some output", "some error" ), ) with pytest.raises(EnvCommandError) as error: tmp_venv.run("python", "-c", MINIMAL_SCRIPT) subprocess.check_output.assert_called_once() # type: ignore[attr-defined] assert "some output" in str(error.value) assert "some error" in str(error.value) def test_call_no_input_with_called_process_error( tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture ) -> None: mocker.patch( "subprocess.check_call", side_effect=subprocess.CalledProcessError( 42, "some_command", "some output", "some error" ), ) kwargs = {"call": True} with pytest.raises(EnvCommandError) as error: tmp_venv.run("python", "-", **kwargs) subprocess.check_call.assert_called_once() # type: ignore[attr-defined] assert "some output" in str(error.value) assert "some error" in str(error.value) def test_check_output_with_called_process_error( tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture ) -> None: mocker.patch( "subprocess.check_output", side_effect=subprocess.CalledProcessError( 42, "some_command", "some output", "some error" ), ) with pytest.raises(EnvCommandError) as error: tmp_venv.run("python", "-") subprocess.check_output.assert_called_once() # type: ignore[attr-defined] assert "some output" in str(error.value) assert "some error" in str(error.value) @pytest.mark.parametrize("out", ["sys.stdout", "sys.stderr"]) def test_call_does_not_block_on_full_pipe( tmp_path: Path, tmp_venv: VirtualEnv, out: str ) -> None: """see https://github.com/python-poetry/poetry/issues/7698""" script = tmp_path / "script.py" script.write_text( f"""\ import sys for i in range(10000): print('just print a lot of text to fill the buffer', file={out}) """, encoding="utf-8", ) def target(result: list[int]) -> None: tmp_venv.run("python", str(script), call=True) result.append(0) results: list[int] = [] # use a separate thread, so that the test does not block in case of error thread = Thread(target=target, args=(results,)) thread.start() thread.join(1) # must not block assert results and results[0] == 0 def test_run_python_script_called_process_error( tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture ) -> None: mocker.patch( "subprocess.run", side_effect=subprocess.CalledProcessError( 42, "some_command", "some output", "some error" ), ) with pytest.raises(EnvCommandError) as error: tmp_venv.run_python_script(MINIMAL_SCRIPT) assert "some output" in str(error.value) assert "some error" in str(error.value) def test_run_python_script_only_stdout(tmp_path: Path, tmp_venv: VirtualEnv) -> None: output = tmp_venv.run_python_script( "import sys; print('some warning', file=sys.stderr); print('some output')" ) assert "some output" in output assert "some warning" not in output def test_system_env_has_correct_paths() -> None: env = SystemEnv(Path(sys.prefix)) paths = env.paths assert paths.get("purelib") is not None assert paths.get("platlib") is not None assert paths.get("scripts") is not None assert env.site_packages.path == Path(paths["purelib"]) assert paths["include"] is not None @pytest.mark.parametrize( "enabled", [True, False], ) def test_system_env_usersite(mocker: MockerFixture, enabled: bool) -> None: mocker.patch("site.check_enableusersite", return_value=enabled) env = SystemEnv(Path(sys.prefix)) assert (enabled and env.usersite is not None) or ( not enabled and env.usersite is None ) def test_venv_has_correct_paths(tmp_venv: VirtualEnv) -> None: paths = tmp_venv.paths assert paths.get("purelib") is not None assert paths.get("platlib") is not None assert paths.get("scripts") is not None assert tmp_venv.site_packages.path == Path(paths["purelib"]) assert paths["include"] == str( tmp_venv.path.joinpath( f"include/site/python{tmp_venv.version_info[0]}.{tmp_venv.version_info[1]}" ) ) @pytest.mark.parametrize("with_system_site_packages", [True, False]) def test_env_system_packages( tmp_path: Path, poetry: Poetry, with_system_site_packages: bool ) -> None: venv_path = tmp_path / "venv" pyvenv_cfg = venv_path / "pyvenv.cfg" EnvManager(poetry).build_venv( path=venv_path, flags={"system-site-packages": with_system_site_packages} ) env = VirtualEnv(venv_path) assert ( f"include-system-site-packages = {str(with_system_site_packages).lower()}" in pyvenv_cfg.read_text(encoding="utf-8") ) assert env.includes_system_site_packages is with_system_site_packages def test_generic_env_system_packages(poetry: Poetry) -> None: """https://github.com/python-poetry/poetry/issues/8646""" env = GenericEnv(Path(sys.base_prefix)) assert not env.includes_system_site_packages @pytest.mark.parametrize("with_system_site_packages", [True, False]) def test_env_system_packages_are_relative_to_lib( tmp_path: Path, poetry: Poetry, with_system_site_packages: bool ) -> None: venv_path = tmp_path / "venv" EnvManager(poetry).build_venv( path=venv_path, flags={"system-site-packages": with_system_site_packages} ) env = VirtualEnv(venv_path) # These are Poetry's own dependencies. # They should not be relative to the virtualenv's lib directory. for dist in metadata.distributions(): assert not env.is_path_relative_to_lib( Path(str(dist._path)) # type: ignore[attr-defined] ) # Checking one package is sufficient break else: pytest.fail("No distributions found in Poetry's own environment") # These are the virtual environments' base env packages, # in this case the system site packages. for dist in env.parent_env.site_packages.distributions(): assert ( env.is_path_relative_to_lib( Path(str(dist._path)) # type: ignore[attr-defined] ) is with_system_site_packages ) # Checking one package is sufficient break else: pytest.fail("No distributions found in the base environment of the virtualenv") @pytest.mark.parametrize( ("flags", "packages"), [ ({"no-pip": False}, {"pip"}), ({"no-pip": True}, set()), ({}, set()), ], ) def test_env_no_pip( tmp_path: Path, poetry: Poetry, flags: dict[str, str | bool], packages: set[str] ) -> None: venv_path = tmp_path / "venv" EnvManager(poetry).build_venv(path=venv_path, flags=flags) env = VirtualEnv(venv_path) installed_repository = InstalledRepository.load(env=env, with_dependencies=True) installed_packages = { package.name for package in installed_repository.packages # workaround for BSD test environments if package.name != "sqlite3" } assert installed_packages == packages def test_env_finds_the_correct_executables(tmp_path: Path, manager: EnvManager) -> None: venv_path = tmp_path / "Virtual Env" manager.build_venv(venv_path, with_pip=True) venv = VirtualEnv(venv_path) default_executable = expected_executable = f"python{'.exe' if WINDOWS else ''}" default_pip_executable = expected_pip_executable = f"pip{'.exe' if WINDOWS else ''}" major_executable = f"python{sys.version_info[0]}{'.exe' if WINDOWS else ''}" major_pip_executable = f"pip{sys.version_info[0]}{'.exe' if WINDOWS else ''}" if ( venv._bin_dir.joinpath(default_executable).exists() and venv._bin_dir.joinpath(major_executable).exists() ): venv._bin_dir.joinpath(default_executable).unlink() expected_executable = major_executable if ( venv._bin_dir.joinpath(default_pip_executable).exists() and venv._bin_dir.joinpath(major_pip_executable).exists() ): venv._bin_dir.joinpath(default_pip_executable).unlink() expected_pip_executable = major_pip_executable venv = VirtualEnv(venv_path) assert Path(venv.python).name == expected_executable assert Path(venv.pip).name.startswith(expected_pip_executable.split(".")[0]) def test_env_finds_the_correct_executables_for_generic_env( tmp_path: Path, manager: EnvManager ) -> None: venv_path = tmp_path / "Virtual Env" child_venv_path = tmp_path / "Child Virtual Env" manager.build_venv(venv_path, with_pip=True) parent_venv = VirtualEnv(venv_path) manager.build_venv(child_venv_path, executable=parent_venv.python, with_pip=True) venv = GenericEnv(parent_venv.path, child_env=VirtualEnv(child_venv_path)) expected_executable = ( f"python{sys.version_info[0]}.exe" if WINDOWS else f"python{sys.version_info[0]}.{sys.version_info[1]}" ) expected_pip_executable = ( f"pip{sys.version_info[0]}.{sys.version_info[1]}{'.exe' if WINDOWS else ''}" ) assert Path(venv.python).name == expected_executable assert Path(venv.pip).name == expected_pip_executable @pytest.mark.parametrize("fallback", ["major", "default"]) def test_env_finds_fallback_executables_for_generic_env( tmp_path: Path, manager: EnvManager, fallback: str ) -> None: venv_path = tmp_path / "Virtual Env" child_venv_path = tmp_path / "Child Virtual Env" manager.build_venv(venv_path, with_pip=True) parent_venv = VirtualEnv(venv_path) manager.build_venv(child_venv_path, executable=parent_venv.python, with_pip=True) venv = GenericEnv(parent_venv.path, child_env=VirtualEnv(child_venv_path)) default_executable = f"python{'.exe' if WINDOWS else ''}" default_pip_executable = f"pip{'.exe' if WINDOWS else ''}" major_executable = f"python{sys.version_info[0]}{'.exe' if WINDOWS else ''}" major_pip_executable = f"pip{sys.version_info[0]}{'.exe' if WINDOWS else ''}" minor_executable = ( f"python{sys.version_info[0]}.{sys.version_info[1]}{'.exe' if WINDOWS else ''}" ) minor_pip_executable = ( f"pip{sys.version_info[0]}.{sys.version_info[1]}{'.exe' if WINDOWS else ''}" ) venv._bin_dir.joinpath(minor_executable).unlink(missing_ok=True) venv._bin_dir.joinpath(minor_pip_executable).unlink(missing_ok=True) if fallback == "default": venv._bin_dir.joinpath(major_executable).unlink(missing_ok=True) venv._bin_dir.joinpath(major_pip_executable).unlink(missing_ok=True) expected_executable = default_executable expected_pip_executable = default_pip_executable else: expected_executable = major_executable expected_pip_executable = major_pip_executable venv = GenericEnv(parent_venv.path, child_env=VirtualEnv(child_venv_path)) assert Path(venv.python).name == expected_executable assert Path(venv.pip).name == expected_pip_executable @pytest.fixture def extended_without_setup_poetry( fixture_dir: FixtureDirGetter, set_project_context: SetProjectContext ) -> Iterator[Poetry]: with set_project_context("extended_project_without_setup") as cwd: yield Factory().create_poetry(cwd) def test_build_environment_called_build_script_specified( mocker: MockerFixture, extended_without_setup_poetry: Poetry, ) -> None: patched_install = mocker.patch("poetry.utils.isolated_build.IsolatedEnv.install") with ephemeral_environment() as project_env: import poetry.utils.env spy = mocker.spy(poetry.utils.env, "ephemeral_environment") with build_environment(extended_without_setup_poetry, project_env): assert patched_install.call_count == 1 assert patched_install.call_args == mocker.call(["poetry-core", "cython"]) assert spy.call_count == 1 def test_build_environment_not_called_without_build_script_specified( mocker: MockerFixture, poetry: Poetry, tmp_path: Path ) -> None: project_env = MockEnv(path=tmp_path / "project") ephemeral_env = MockEnv(path=tmp_path / "ephemeral") mocker.patch( "poetry.utils.env.ephemeral_environment" ).return_value.__enter__.return_value = ephemeral_env with build_environment(poetry, project_env) as env: assert env == project_env assert not env.executed # type: ignore[attr-defined] def test_command_from_bin_preserves_relative_path(manager: EnvManager) -> None: # https://github.com/python-poetry/poetry/issues/7959 env = manager.get() command = env.get_command_from_bin("./foo.py") assert command == ["./foo.py"] @pytest.fixture def system_env_read_only(system_env: SystemEnv, mocker: MockerFixture) -> SystemEnv: original_is_dir_writable = is_dir_writable read_only_paths = {system_env.paths[key] for key in SCHEME_NAMES} def mock_is_dir_writable(path: Path, create: bool = False) -> bool: if str(path) in read_only_paths: return False return original_is_dir_writable(path, create) mocker.patch("poetry.utils.env.base_env.is_dir_writable", new=mock_is_dir_writable) return system_env def test_env_scheme_dict_returns_original_when_writable(system_env: SystemEnv) -> None: assert not DeepDiff(system_env.scheme_dict, system_env.paths, ignore_order=True) def test_env_scheme_dict_returns_modified_when_read_only( system_env_read_only: SystemEnv, ) -> None: scheme_dict = system_env_read_only.scheme_dict assert DeepDiff(scheme_dict, system_env_read_only.paths, ignore_order=True) paths = system_env_read_only.paths assert all( Path(scheme_dict[scheme]).exists() and scheme_dict[scheme].startswith(paths["userbase"]) for scheme in SCHEME_NAMES ) def test_marker_env_is_equal_for_all_envs(tmp_path: Path, manager: EnvManager) -> None: venv_path = tmp_path / "Virtual Env" manager.build_venv(venv_path) venv = VirtualEnv(venv_path) generic_env = GenericEnv(venv.path) system_env = SystemEnv(Path(sys.prefix)) venv_marker_env = venv.marker_env generic_marker_env = generic_env.marker_env system_marker_env = system_env.marker_env assert venv_marker_env == generic_marker_env assert venv_marker_env == system_marker_env ================================================ FILE: tests/utils/env/test_env_manager.py ================================================ from __future__ import annotations import logging import os import sys from pathlib import Path from typing import TYPE_CHECKING from typing import Any import pytest import tomlkit from poetry.core.constraints.version import Version from poetry.config.config import Config from poetry.console.exceptions import PoetryConsoleError from poetry.toml.file import TOMLFile from poetry.utils.env import GET_BASE_PREFIX from poetry.utils.env import GET_PYTHON_VERSION_ONELINER from poetry.utils.env import EnvManager from poetry.utils.env import IncorrectEnvError from poetry.utils.env.env_manager import EnvsFile from poetry.utils.env.python.exceptions import InvalidCurrentPythonVersionError from poetry.utils.env.python.exceptions import NoCompatiblePythonVersionFoundError from poetry.utils.env.python.exceptions import PythonVersionNotFoundError from poetry.utils.helpers import remove_directory if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterator from unittest.mock import MagicMock from pytest import LogCaptureFixture from pytest_mock import MockerFixture from poetry.poetry import Poetry from tests.conftest import Config from tests.types import FixtureDirGetter from tests.types import MockedPythonRegister from tests.types import ProjectFactory VERSION_3_7_1 = Version.parse("3.7.1") def build_venv(path: Path | str, **__: Any) -> None: os.mkdir(str(path)) def check_output_wrapper( version: Version = VERSION_3_7_1, ) -> Callable[[list[str], Any, Any], str]: def check_output(cmd: list[str], *args: Any, **kwargs: Any) -> str: # cmd is a list, like ["python", "-c", "do stuff"] python_cmd = cmd[-1] if "print(json.dumps(env))" in python_cmd: return ( f'{{"version_info": [{version.major}, {version.minor},' f" {version.patch}]}}" ) if "sys.version_info[:3]" in python_cmd: return version.text if "sys.version_info[:2]" in python_cmd: return f"{version.major}.{version.minor}" if "import sys; print(sys.executable)" in python_cmd: executable = cmd[0] basename = os.path.basename(executable) return f"/usr/bin/{basename}" if "print(sys.base_prefix)" in python_cmd: return sys.base_prefix assert "import sys; print(sys.prefix)" in python_cmd return "/prefix" return check_output @pytest.fixture def in_project_venv_dir(poetry: Poetry) -> Iterator[Path]: os.environ.pop("VIRTUAL_ENV", None) venv_dir = poetry.file.path.parent.joinpath(".venv") venv_dir.mkdir() try: yield venv_dir finally: venv_dir.rmdir() @pytest.mark.parametrize( ("section", "version", "expected"), [ ("foo", None, "3.10"), ("bar", None, "3.11"), ("baz", None, "3.12"), ("bar", "3.11", "3.11"), ("bar", "3.10", None), ], ) def test_envs_file_remove_section( tmp_path: Path, section: str, version: str | None, expected: str | None ) -> None: envs_file_path = tmp_path / "envs.toml" envs_file = TOMLFile(envs_file_path) doc = tomlkit.document() doc["foo"] = {"minor": "3.10", "patch": "3.10.13"} doc["bar"] = {"minor": "3.11", "patch": "3.11.7"} doc["baz"] = {"minor": "3.12", "patch": "3.12.1"} envs_file.write(doc) minor = EnvsFile(envs_file_path).remove_section(section, version) assert minor == expected envs = TOMLFile(envs_file_path).read() if expected is None: assert section in envs else: assert section not in envs for other_section in {"foo", "bar", "baz"} - {section}: assert other_section in envs def test_activate_in_project_venv_no_explicit_config( tmp_path: Path, manager: EnvManager, poetry: Poetry, mocker: MockerFixture, venv_name: str, in_project_venv_dir: Path, mocked_python_register: MockedPythonRegister, ) -> None: mocked_python_register("3.7.1") m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) env = manager.activate("python3.7") assert env.path == tmp_path / "poetry-fixture-simple" / ".venv" assert env.base == Path(sys.base_prefix) m.assert_called_with( tmp_path / "poetry-fixture-simple" / ".venv", executable=Path("/usr/bin/python3.7"), flags={ "always-copy": False, "system-site-packages": False, "no-pip": False, }, prompt="simple-project-py3.7", ) envs_file = TOMLFile(tmp_path / "envs.toml") assert not envs_file.exists() def test_activate_activates_non_existing_virtualenv_no_envs_file( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, venv_flags_default: dict[str, bool], mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocked_python_register("3.7.1") m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) env = manager.activate("python3.7") m.assert_called_with( tmp_path / f"{venv_name}-py3.7", executable=Path("/usr/bin/python3.7"), flags=venv_flags_default, prompt="simple-project-py3.7", ) envs_file = TOMLFile(tmp_path / "envs.toml") assert envs_file.exists() envs: dict[str, Any] = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path(sys.base_prefix) def test_activate_fails_when_python_cannot_be_found( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] os.mkdir(tmp_path / f"{venv_name}-py3.7") config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocked_python_register("2.7.1") with pytest.raises(PythonVersionNotFoundError) as e: manager.activate("python3.7") expected_message = "Could not find the python executable python3.7" assert str(e.value) == expected_message def test_activate_activates_existing_virtualenv_no_envs_file( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] os.mkdir(tmp_path / f"{venv_name}-py3.7") config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocked_python_register("3.7.1") m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) env = manager.activate("python3.7") m.assert_not_called() envs_file = TOMLFile(tmp_path / "envs.toml") assert envs_file.exists() envs: dict[str, Any] = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path(sys.base_prefix) def test_activate_activates_same_virtualenv_with_envs_file( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.1"} envs_file.write(doc) os.mkdir(tmp_path / f"{venv_name}-py3.7") config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocked_python_register("3.7.1") m = mocker.patch("poetry.utils.env.EnvManager.create_venv") env = manager.activate("python3.7") m.assert_not_called() assert envs_file.exists() envs: dict[str, Any] = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path(sys.base_prefix) def test_activate_activates_different_virtualenv_with_envs_file( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, venv_flags_default: dict[str, bool], mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.1"} envs_file.write(doc) os.mkdir(tmp_path / f"{venv_name}-py3.7") config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocked_python_register("3.6.6") mocked_python_register("3.7.1") m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) env = manager.activate("python3.6") m.assert_called_with( tmp_path / f"{venv_name}-py3.6", executable=Path("/usr/bin/python3.6"), flags=venv_flags_default, prompt="simple-project-py3.6", ) assert envs_file.exists() envs: dict[str, Any] = envs_file.read() assert envs[venv_name]["minor"] == "3.6" assert envs[venv_name]["patch"] == "3.6.6" assert env.path == tmp_path / f"{venv_name}-py3.6" assert env.base == Path(sys.base_prefix) def test_activate_activates_recreates_for_different_patch( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, venv_flags_default: dict[str, bool], mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) os.mkdir(tmp_path / f"{venv_name}-py3.7") config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocked_python_register("3.7.1") build_venv_m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=build_venv ) remove_venv_m = mocker.patch( "poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv ) env = manager.activate("python3.7") build_venv_m.assert_called_with( tmp_path / f"{venv_name}-py3.7", executable=Path("/usr/bin/python3.7"), flags=venv_flags_default, prompt="simple-project-py3.7", ) remove_venv_m.assert_called_with(tmp_path / f"{venv_name}-py3.7") assert envs_file.exists() envs: dict[str, Any] = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path(sys.base_prefix) assert (tmp_path / f"{venv_name}-py3.7").exists() def test_activate_does_not_recreate_when_switching_minor( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) os.mkdir(tmp_path / f"{venv_name}-py3.7") os.mkdir(tmp_path / f"{venv_name}-py3.6") config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocked_python_register("3.7.1") mocked_python_register("3.6.6") build_venv_m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=build_venv ) remove_venv_m = mocker.patch( "poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv ) env = manager.activate("python3.6") build_venv_m.assert_not_called() remove_venv_m.assert_not_called() assert envs_file.exists() envs: dict[str, Any] = envs_file.read() assert envs[venv_name]["minor"] == "3.6" assert envs[venv_name]["patch"] == "3.6.6" assert env.path == tmp_path / f"{venv_name}-py3.6" assert env.base == Path(sys.base_prefix) assert (tmp_path / f"{venv_name}-py3.6").exists() def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( manager: EnvManager, poetry: Poetry, config: Config, tmp_path: Path, mocker: MockerFixture, venv_flags_default: dict[str, bool], mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] config.merge( { "virtualenvs": { "path": str(tmp_path / "virtualenvs"), "in-project": True, } } ) mocked_python_register("3.7.1") m = mocker.patch("poetry.utils.env.EnvManager.build_venv") manager.activate("python3.7") m.assert_called_with( poetry.file.path.parent / ".venv", executable=Path("/usr/bin/python3.7"), flags=venv_flags_default, prompt="simple-project-py3.7", ) envs_file = TOMLFile(tmp_path / "virtualenvs" / "envs.toml") assert not envs_file.exists() def test_deactivate_non_activated_but_existing( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, venv_name: str, ) -> None: config.config["virtualenvs"]["use-poetry-python"] = True if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] python = ".".join(str(c) for c in sys.version_info[:2]) (tmp_path / f"{venv_name}-py{python}").mkdir() config.merge({"virtualenvs": {"path": str(tmp_path)}}) manager.deactivate() env = manager.get() assert env.path == tmp_path / f"{venv_name}-py{python}" def test_deactivate_activated( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, ) -> None: config.config["virtualenvs"]["use-poetry-python"] = True if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] version = Version.from_parts(*sys.version_info[:3]) other_version = Version.parse("3.4") if version.major == 2 else version.next_minor() (tmp_path / f"{venv_name}-py{version.major}.{version.minor}").mkdir() (tmp_path / f"{venv_name}-py{other_version.major}.{other_version.minor}").mkdir() envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = { "minor": f"{other_version.major}.{other_version.minor}", "patch": other_version.text, } envs_file.write(doc) config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), ) manager.deactivate() env = manager.get() assert env.path == tmp_path / f"{venv_name}-py{version.major}.{version.minor}" envs = envs_file.read() assert len(envs) == 0 @pytest.mark.parametrize("in_project", [True, False, None]) def test_get_venv_with_venv_folder_present( manager: EnvManager, poetry: Poetry, in_project_venv_dir: Path, in_project: bool | None, ) -> None: poetry.config.config["virtualenvs"]["in-project"] = in_project venv = manager.get() if in_project is False: assert venv.path != in_project_venv_dir else: assert venv.path == in_project_venv_dir def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, ) -> None: os.environ["VIRTUAL_ENV"] = "/environment/prefix" config.merge({"virtualenvs": {"path": str(tmp_path)}}) (tmp_path / f"{venv_name}-py3.7").mkdir() envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), ) env = manager.get() assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path(sys.base_prefix) def test_list( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, venv_name: str, ) -> None: config.merge({"virtualenvs": {"path": str(tmp_path)}}) (tmp_path / f"{venv_name}-py3.7").mkdir() (tmp_path / f"{venv_name}-py3.6").mkdir() venvs = manager.list() assert len(venvs) == 2 assert venvs[0].path == tmp_path / f"{venv_name}-py3.6" assert venvs[1].path == tmp_path / f"{venv_name}-py3.7" def test_remove_by_python_version( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, ) -> None: config.merge({"virtualenvs": {"path": str(tmp_path)}}) (tmp_path / f"{venv_name}-py3.7").mkdir() (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) venv = manager.remove("3.6") expected_venv_path = tmp_path / f"{venv_name}-py3.6" assert venv.path == expected_venv_path assert not expected_venv_path.exists() def test_remove_by_name( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, ) -> None: config.merge({"virtualenvs": {"path": str(tmp_path)}}) (tmp_path / f"{venv_name}-py3.7").mkdir() (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) venv = manager.remove(f"{venv_name}-py3.6") expected_venv_path = tmp_path / f"{venv_name}-py3.6" assert venv.path == expected_venv_path assert not expected_venv_path.exists() def test_remove_by_string_with_python_and_version( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, ) -> None: config.merge({"virtualenvs": {"path": str(tmp_path)}}) (tmp_path / f"{venv_name}-py3.7").mkdir() (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) venv = manager.remove("python3.6") expected_venv_path = tmp_path / f"{venv_name}-py3.6" assert venv.path == expected_venv_path assert not expected_venv_path.exists() def test_remove_by_full_path_to_python( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, ) -> None: config.merge({"virtualenvs": {"path": str(tmp_path)}}) (tmp_path / f"{venv_name}-py3.7").mkdir() (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) expected_venv_path = tmp_path / f"{venv_name}-py3.6" python_path = expected_venv_path / "bin" / "python" venv = manager.remove(str(python_path)) assert venv.path == expected_venv_path assert not expected_venv_path.exists() def test_remove_raises_if_acting_on_different_project_by_full_path( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, ) -> None: config.merge({"virtualenvs": {"path": str(tmp_path)}}) different_venv_name = "different-project" different_venv_path = tmp_path / f"{different_venv_name}-py3.6" different_venv_bin_path = different_venv_path / "bin" different_venv_bin_path.mkdir(parents=True) python_path = different_venv_bin_path / "python" python_path.touch(exist_ok=True) # Patch initial call where python env path is extracted mocker.patch( "subprocess.check_output", side_effect=lambda *args, **kwargs: str(different_venv_path), ) with pytest.raises(IncorrectEnvError): manager.remove(str(python_path)) def test_remove_raises_if_acting_on_different_project_by_name( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, ) -> None: config.merge({"virtualenvs": {"path": str(tmp_path)}}) different_venv_name = ( EnvManager.generate_env_name( "different-project", str(poetry.file.path.parent), ) + "-py3.6" ) different_venv_path = tmp_path / different_venv_name different_venv_bin_path = different_venv_path / "bin" different_venv_bin_path.mkdir(parents=True) python_path = different_venv_bin_path / "python" python_path.touch(exist_ok=True) with pytest.raises(IncorrectEnvError): manager.remove(different_venv_name) def test_raises_when_passing_old_env_after_dir_rename( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, venv_name: str, ) -> None: # Make sure that poetry raises when trying to remove old venv after you've renamed # root directory of the project, which will create another venv with new name. # This is not ideal as you still "can't" remove it by name, but it at least doesn't # cause any unwanted side effects config.merge({"virtualenvs": {"path": str(tmp_path)}}) previous_venv_name = EnvManager.generate_env_name( poetry.package.name, "previous_dir_name", ) venv_path = tmp_path / f"{venv_name}-py3.6" venv_path.mkdir() previous_venv_name = f"{previous_venv_name}-py3.6" previous_venv_path = tmp_path / previous_venv_name previous_venv_path.mkdir() with pytest.raises(IncorrectEnvError): manager.remove(previous_venv_name) def test_remove_also_deactivates( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, ) -> None: config.merge({"virtualenvs": {"path": str(tmp_path)}}) (tmp_path / f"{venv_name}-py3.7").mkdir() (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.6", "patch": "3.6.6"} envs_file.write(doc) venv = manager.remove("python3.6") expected_venv_path = tmp_path / f"{venv_name}-py3.6" assert venv.path == expected_venv_path assert not expected_venv_path.exists() envs = envs_file.read() assert venv_name not in envs def test_remove_keeps_dir_if_not_deleteable( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, ) -> None: # Ensure we empty rather than delete folder if its is an active mount point. # See https://github.com/python-poetry/poetry/pull/2064 config.merge({"virtualenvs": {"path": str(tmp_path)}}) venv_path = tmp_path / f"{venv_name}-py3.6" venv_path.mkdir() folder1_path = venv_path / "folder1" folder1_path.mkdir() file1_path = folder1_path / "file1" file1_path.touch(exist_ok=False) file2_path = venv_path / "file2" file2_path.touch(exist_ok=False) mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) def err_on_rm_venv_only(path: Path, *args: Any, **kwargs: Any) -> None: if path.resolve() == venv_path.resolve(): raise OSError(16, "Test error") # ERRNO 16: Device or resource busy else: remove_directory(path) m = mocker.patch( "poetry.utils.env.env_manager.remove_directory", side_effect=err_on_rm_venv_only ) venv = manager.remove(f"{venv_name}-py3.6") m.assert_any_call(venv_path) assert venv_path == venv.path assert venv_path.exists() assert not folder1_path.exists() assert not file1_path.exists() assert not file2_path.exists() m.side_effect = remove_directory # Avoid teardown using `err_on_rm_venv_only` def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ones_first( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, venv_flags_default: dict[str, bool], mocked_python_register: MockedPythonRegister, ) -> None: config.config["virtualenvs"]["use-poetry-python"] = True if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "^3.6" mocked_python_register("2.7.16", make_system=True) mocked_python_register("3.7.16", "python3") m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.7", executable=Path("/usr/bin/python3"), flags=venv_flags_default, prompt="simple-project-py3.7", ) def test_create_venv_finds_no_python_executable( manager: EnvManager, poetry: Poetry, config: Config, config_virtualenvs_path: Path, venv_name: str, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "^999" with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv() expected_message = ( "Poetry was unable to find a compatible version. " "If you have one, you can explicitly use it " 'via the "env use" command.' ) assert str(e.value) == expected_message def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific_ones( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, venv_flags_default: dict[str, bool], mocked_python_register: MockedPythonRegister, ) -> None: config.config["virtualenvs"]["use-poetry-python"] = True if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "^3.6" mocked_python_register("3.5.3") mocked_python_register("3.9.0") mocker.patch( "poetry.utils.env.python.Python.get_system_python", return_value=mocked_python_register("2.7.16", make_system=True), ) mocked_python_register("3.5.3") mocked_python_register("3.9.0") m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.9", executable=Path("/usr/bin/python3.9"), flags=venv_flags_default, prompt="simple-project-py3.9", ) def test_create_venv_fails_if_no_compatible_python_version_could_be_found( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture ) -> None: config.config["virtualenvs"]["use-poetry-python"] = True if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "^4.8" mocker.patch( "subprocess.check_output", side_effect=[sys.base_prefix, "/usr/bin/python", "3.9.0"], ) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv() expected_message = ( "Poetry was unable to find a compatible version. " "If you have one, you can explicitly use it " 'via the "env use" command.' ) assert str(e.value) == expected_message assert m.call_count == 0 @pytest.mark.parametrize("use_poetry_python", [True, False]) def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, mocked_python_register: MockedPythonRegister, use_poetry_python: bool, ) -> None: config.config["virtualenvs"]["use-poetry-python"] = use_poetry_python if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "^4.8" m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv(python=mocked_python_register("3.8.0")) expected_message = ( "The specified Python version (3.8.0) is not supported by the project (^4.8).\n" "Please choose a compatible version or loosen the python constraint " "specified in the pyproject.toml file." ) assert str(e.value) == expected_message assert m.call_count == 0 def test_create_venv_uses_patch_version_to_detect_compatibility( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, venv_flags_default: dict[str, bool], mocked_python_register: MockedPythonRegister, ) -> None: config.config["virtualenvs"]["use-poetry-python"] = True if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] version = Version.from_parts(*sys.version_info[:3]) poetry.package.python_versions = "^" + ".".join( str(c) for c in sys.version_info[:3] ) assert version.patch is not None python = mocked_python_register( f"{version.major}.{version.minor}.{version.patch + 1}" ) mocker.patch( "poetry.utils.env.python.Python.get_system_python", return_value=python, ) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py{version.major}.{version.minor}", executable=python.executable, flags=venv_flags_default, prompt=f"simple-project-py{version.major}.{version.minor}", ) def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, venv_flags_default: dict[str, bool], mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] version = Version.from_parts(*sys.version_info[:3]) assert version.minor is not None poetry.package.python_versions = "~3.6.0" venv_name = manager.generate_env_name( "simple-project", str(poetry.file.path.parent) ) mocked_python_register("3.6.0") m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) manager.create_venv(python=mocked_python_register("3.6.0")) m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.6", executable=Path("/usr/bin/python3.6"), flags=venv_flags_default, prompt="simple-project-py3.6", ) def test_create_venv_fails_if_current_python_version_is_not_supported( manager: EnvManager, poetry: Poetry ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] manager.create_venv() current_version = Version.parse(".".join(str(c) for c in sys.version_info[:3])) assert current_version.minor is not None next_version = ".".join( str(c) for c in (current_version.major, current_version.minor + 1, 0) ) package_version = "~" + next_version poetry.package.python_versions = package_version with pytest.raises(InvalidCurrentPythonVersionError) as e: manager.create_venv() expected_message = ( f"Current Python version ({current_version}) is not allowed by the project" f' ({package_version}).\nPlease change python executable via the "env use"' " command." ) assert expected_message == str(e.value) def test_create_venv_project_name_empty_sets_correct_prompt( fixture_dir: FixtureDirGetter, project_factory: ProjectFactory, config: Config, mocker: MockerFixture, config_virtualenvs_path: Path, mocked_python_register: MockedPythonRegister, ) -> None: config.config["virtualenvs"]["use-poetry-python"] = True if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry = project_factory("no", source=fixture_dir("no_name_project")) manager = EnvManager(poetry) poetry.package.python_versions = "^3.7" venv_name = manager.generate_env_name( "non-package-mode", str(poetry.file.path.parent) ) mocked_python_register("2.7.16", make_system=True) mocked_python_register("3.7.1", "python3") m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.7", executable=Path("/usr/bin/python3"), flags={ "always-copy": False, "system-site-packages": False, "no-pip": False, }, prompt="non-package-mode-py3.7", ) def test_create_venv_accepts_fallback_version_w_nonzero_patchlevel( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, mocked_python_register: MockedPythonRegister, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "~3.5.1" def mock_check_output(cmd: str, *args: Any, **kwargs: Any) -> str: if GET_PYTHON_VERSION_ONELINER in cmd: executable = cmd[0] if "python3.5" in str(executable): return "3.5.12" return "3.7.1" if GET_BASE_PREFIX in cmd: return sys.base_prefix return "/usr/bin/python3.5" mocked_python_register("3.5.12") m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.5", executable=Path("/usr/bin/python3.5"), flags={ "always-copy": False, "system-site-packages": False, "no-pip": False, }, prompt="simple-project-py3.5", ) @pytest.mark.parametrize("is_inconsistent_entry", [False, True]) def test_create_venv_does_not_keep_inconsistent_envs_entry( tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, is_inconsistent_entry: bool, mocked_python_register: MockedPythonRegister, with_no_active_python: MagicMock, ) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] # There is an entry in the envs.toml file but the venv does not exist envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() if is_inconsistent_entry: doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} doc["other"] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) mocked_python_register("3.7.0") config.merge({"virtualenvs": {"path": str(tmp_path)}}) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) manager.create_venv() m.assert_called() assert envs_file.exists() envs: dict[str, Any] = envs_file.read() assert venv_name not in envs assert envs["other"]["minor"] == "3.7" assert envs["other"]["patch"] == "3.7.0" def test_build_venv_does_not_change_loglevel( tmp_path: Path, manager: EnvManager, caplog: LogCaptureFixture ) -> None: # see https://github.com/python-poetry/poetry/pull/8760 venv_path = tmp_path / "venv" caplog.set_level(logging.DEBUG) manager.build_venv(venv_path) assert logging.root.level == logging.DEBUG @pytest.mark.skipif(sys.platform != "darwin", reason="requires darwin") def test_venv_backup_exclusion(tmp_path: Path, manager: EnvManager) -> None: import xattr venv_path = tmp_path / "Virtual Env" manager.build_venv(venv_path) value = ( b"bplist00_\x10\x11com.apple.backupd" b"\x08\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00" b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c" ) assert ( xattr.getxattr( str(venv_path), "com.apple.metadata:com_apple_backup_excludeItem" ) == value ) def test_generate_env_name_ignores_case_for_case_insensitive_fs( poetry: Poetry, tmp_path: Path, ) -> None: venv_name1 = EnvManager.generate_env_name(poetry.package.name, "MyDiR") venv_name2 = EnvManager.generate_env_name(poetry.package.name, "mYdIr") if sys.platform == "win32": assert venv_name1 == venv_name2 else: assert venv_name1 != venv_name2 def test_generate_env_name_uses_real_path( tmp_path: Path, mocker: MockerFixture ) -> None: mocker.patch("os.path.realpath", return_value="the_real_dir") venv_name1 = EnvManager.generate_env_name("simple-project", "the_real_dir") venv_name2 = EnvManager.generate_env_name("simple-project", "linked_dir") assert venv_name1 == venv_name2 def test_create_venv_invalid_prompt_template_variable( manager: EnvManager, poetry: Poetry, config: Config ) -> None: config.merge({"virtualenvs": {"prompt": "{project_name}-{invalid_var}"}}) with pytest.raises(PoetryConsoleError) as exc_info: manager.create_venv() assert "Invalid template variable 'invalid_var'" in str(exc_info.value) assert "Valid variables are: {project_name}, {python_version}" in str( exc_info.value ) def test_create_venv_malformed_prompt_template( manager: EnvManager, poetry: Poetry, config: Config ) -> None: config.merge({"virtualenvs": {"prompt": "{project_name"}}) # Missing closing brace with pytest.raises(PoetryConsoleError) as exc_info: manager.create_venv() assert "Invalid template string in 'virtualenvs.prompt' setting" in str( exc_info.value ) ================================================ FILE: tests/utils/env/test_env_site_packages.py ================================================ from __future__ import annotations import uuid from pathlib import Path from typing import TYPE_CHECKING from poetry.utils.env import SitePackages if TYPE_CHECKING: from pytest_mock import MockerFixture def test_env_site_simple(tmp_path: Path, mocker: MockerFixture) -> None: # emulate permission error when creating directory mocker.patch("pathlib.Path.mkdir", side_effect=OSError()) site_packages = SitePackages(Path("/non-existent"), fallbacks=[tmp_path]) candidates = site_packages.make_candidates(Path("hello.txt"), writable_only=True) hello = tmp_path / "hello.txt" assert len(candidates) == 1 assert candidates[0].as_posix() == hello.as_posix() content = str(uuid.uuid4()) site_packages.write_text(Path("hello.txt"), content, encoding="utf-8") assert hello.read_text(encoding="utf-8") == content assert not (site_packages.path / "hello.txt").exists() def test_env_site_select_first(tmp_path: Path) -> None: fallback = tmp_path / "fallback" fallback.mkdir(parents=True) site_packages = SitePackages(tmp_path, fallbacks=[fallback]) candidates = site_packages.make_candidates(Path("hello.txt"), writable_only=True) assert len(candidates) == 2 assert len(site_packages.find(Path("hello.txt"))) == 0 content = str(uuid.uuid4()) site_packages.write_text(Path("hello.txt"), content, encoding="utf-8") assert (site_packages.path / "hello.txt").exists() assert not (fallback / "hello.txt").exists() assert len(site_packages.find(Path("hello.txt"))) == 1 ================================================ FILE: tests/utils/env/test_system_env.py ================================================ from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING from poetry.utils.env import SystemEnv if TYPE_CHECKING: from pytest_mock import MockerFixture def test_get_marker_env_untagged_cpython(mocker: MockerFixture) -> None: mocker.patch("platform.python_version", return_value="3.11.9+") env = SystemEnv(Path(sys.prefix)) marker_env = env.get_marker_env() assert marker_env["python_full_version"] == "3.11.9" ================================================ FILE: tests/utils/fixtures/pyproject.toml ================================================ [tool.poetry] name = "poetry" version = "0.2.0" description = "Python dependency management and packaging made easy." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.md" homepage = "https://python-poetry.org/" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] # Requirements [tool.poetry.dependencies] python = "^3.6" cleo = "^0.6" requests = "^2.18" toml = "^0.9" cachy = "^0.1.0" pip-tools = "^1.11" [tool.poetry.group.dev.dependencies] pytest = "~3.4" ================================================ FILE: tests/utils/test_authenticator.py ================================================ from __future__ import annotations import base64 import logging import re import uuid from pathlib import Path from typing import TYPE_CHECKING from typing import NoReturn import pytest import requests import responses from cleo.io.null_io import NullIO from keyring.credentials import SimpleCredential from poetry.console.exceptions import PoetryRuntimeError from poetry.utils.authenticator import Authenticator from poetry.utils.authenticator import RepositoryCertificateConfig from poetry.utils.password_manager import PoetryKeyring if TYPE_CHECKING: from pytest import LogCaptureFixture from pytest import MonkeyPatch from pytest_mock import MockerFixture from tests.conftest import Config from tests.conftest import DummyBackend from tests.types import HttpResponse @pytest.fixture() def mock_remote(http: responses.RequestsMock) -> None: http.get( re.compile(r"^https?://(?:[^@]+@)?foo\.bar/(.+?)$"), body="", ) @pytest.fixture() def repo() -> dict[str, dict[str, str]]: return {"foo": {"url": "https://foo.bar/simple/"}} @pytest.fixture def mock_config(config: Config, repo: dict[str, dict[str, str]]) -> Config: config.merge( { "repositories": repo, "http-basic": {"foo": {"username": "bar", "password": "baz"}}, } ) return config def test_authenticator_uses_url_provided_credentials( mock_config: Config, mock_remote: None, http: responses.RequestsMock ) -> None: authenticator = Authenticator(mock_config, NullIO()) authenticator.request("get", "https://foo001:bar002@foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"foo001:bar002").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_uses_credentials_from_config_if_not_provided( mock_config: Config, mock_remote: None, http: responses.RequestsMock ) -> None: authenticator = Authenticator(mock_config, NullIO()) authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"bar:baz").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_uses_username_only_credentials( mock_config: Config, mock_remote: None, http: responses.RequestsMock, with_simple_keyring: None, ) -> None: authenticator = Authenticator(mock_config, NullIO()) authenticator.request("get", "https://foo001@foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"foo001:").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_ignores_locked_keyring( mock_remote: None, http: responses.RequestsMock, with_locked_keyring: None, caplog: LogCaptureFixture, mocker: MockerFixture, ) -> None: caplog.set_level(logging.DEBUG, logger="poetry.utils.password_manager") spy_get_credential = mocker.spy(PoetryKeyring, "get_credential") spy_get_password = mocker.spy(PoetryKeyring, "get_password") authenticator = Authenticator() authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request assert "Authorization" not in request.headers assert "Accessing keyring failed during availability check" in caplog.messages assert "Using keyring backend 'conftest LockedBackend'" in caplog.messages assert spy_get_credential.call_count == spy_get_password.call_count == 0 def test_authenticator_ignores_failing_keyring( mock_remote: None, http: responses.RequestsMock, with_erroneous_keyring: None, caplog: LogCaptureFixture, mocker: MockerFixture, ) -> None: caplog.set_level(logging.DEBUG, logger="poetry.utils.password_manager") spy_get_credential = mocker.spy(PoetryKeyring, "get_credential") spy_get_password = mocker.spy(PoetryKeyring, "get_password") authenticator = Authenticator() authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request assert "Authorization" not in request.headers assert "Using keyring backend 'conftest ErroneousBackend'" in caplog.messages assert "Accessing keyring failed during availability check" in caplog.messages assert spy_get_credential.call_count == spy_get_password.call_count == 0 def test_authenticator_uses_password_only_credentials( mock_config: Config, mock_remote: None, http: responses.RequestsMock ) -> None: authenticator = Authenticator(mock_config, NullIO()) authenticator.request("get", "https://:bar002@foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b":bar002").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_uses_empty_strings_as_default_password( config: Config, mock_remote: None, repo: dict[str, dict[str, str]], http: responses.RequestsMock, with_simple_keyring: None, ) -> None: config.merge( { "repositories": repo, "http-basic": {"foo": {"username": "bar"}}, } ) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"bar:").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_does_not_ignore_empty_strings_as_default_username( config: Config, mock_remote: None, repo: dict[str, dict[str, str]], http: responses.RequestsMock, ) -> None: config.merge( { "repositories": repo, "http-basic": {"foo": {"username": None, "password": "bar"}}, } ) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b":bar").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_falls_back_to_keyring_url( config: Config, mock_remote: None, repo: dict[str, dict[str, str]], http: responses.RequestsMock, with_simple_keyring: None, dummy_keyring: DummyBackend, ) -> None: config.merge( { "repositories": repo, } ) dummy_keyring.set_default_service_credential( "https://foo.bar/simple/", SimpleCredential("foo", "bar"), ) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"foo:bar").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_falls_back_to_keyring_netloc( config: Config, mock_remote: None, repo: dict[str, dict[str, str]], http: responses.RequestsMock, with_simple_keyring: None, dummy_keyring: DummyBackend, poetry_keyring: PoetryKeyring, ) -> None: config.merge( { "repositories": repo, } ) dummy_keyring.set_default_service_credential( "foo.bar", SimpleCredential("foo", "bar"), ) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"foo:bar").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" @pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") def test_authenticator_request_retries_on_exception( mocker: MockerFixture, config: Config, http: responses.RequestsMock ) -> None: sleep = mocker.patch("time.sleep") sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" content = str(uuid.uuid4()) seen: list[str] = [] def callback(request: requests.PreparedRequest) -> HttpResponse: assert request.url if seen.count(request.url) < 2: seen.append(request.url) raise requests.exceptions.ConnectionError("Disconnected") return 200, {}, content http.add_callback(responses.GET, sdist_uri, callback=callback) authenticator = Authenticator(config, NullIO()) response = authenticator.request("get", sdist_uri) assert response.text == content assert sleep.call_count == 2 @pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") def test_authenticator_request_raises_exception_when_attempts_exhausted( mocker: MockerFixture, config: Config, http: responses.RequestsMock ) -> None: sleep = mocker.patch("time.sleep") sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" def callback(request: requests.PreparedRequest) -> NoReturn: raise requests.exceptions.ConnectionError(str(uuid.uuid4())) http.add_callback(responses.GET, sdist_uri, callback=callback) authenticator = Authenticator(config, NullIO()) with pytest.raises(PoetryRuntimeError) as e: authenticator.request("get", sdist_uri) assert str(e.value) == "All attempts to connect to foo.bar failed." assert sleep.call_count == 5 def test_authenticator_request_respects_retry_header( mocker: MockerFixture, config: Config, http: responses.RequestsMock, ) -> None: sleep = mocker.patch("time.sleep") sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" content = str(uuid.uuid4()) seen: list[str] = [] def callback(request: requests.PreparedRequest) -> HttpResponse: assert request.url if not seen.count(request.url): seen.append(request.url) return 429, {"Retry-After": "42"}, "Retry later" return 200, {}, content http.add_callback(responses.GET, sdist_uri, callback=callback) authenticator = Authenticator(config, NullIO()) response = authenticator.request("get", sdist_uri) assert sleep.call_args[0] == (42.0,) assert response.text == content @pytest.mark.parametrize( ["status", "attempts"], [ (400, 0), (401, 0), (403, 0), (404, 0), (429, 5), (500, 5), (501, 5), (502, 5), (503, 5), (504, 5), ], ) def test_authenticator_request_retries_on_status_code( mocker: MockerFixture, config: Config, http: responses.RequestsMock, status: int, attempts: int, ) -> None: sleep = mocker.patch("time.sleep") sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" content = str(uuid.uuid4()) def callback(request: requests.PreparedRequest) -> HttpResponse: return status, {}, content http.add_callback(responses.GET, sdist_uri, callback=callback) authenticator = Authenticator(config, NullIO()) with pytest.raises(requests.exceptions.HTTPError) as excinfo: authenticator.request("get", sdist_uri) assert excinfo.value.response is not None assert excinfo.value.response.status_code == status assert excinfo.value.response.text == content assert sleep.call_count == attempts def test_authenticator_uses_env_provided_credentials( config: Config, repo: dict[str, dict[str, str]], environ: None, mock_remote: responses.RequestsMock, http: responses.RequestsMock, monkeypatch: MonkeyPatch, ) -> None: monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_USERNAME", "bar") monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_PASSWORD", "baz") config.merge({"repositories": repo}) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"bar:baz").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" @pytest.mark.parametrize( "cert,client_cert", [ (None, None), (None, "path/to/provided/client-cert"), ("/path/to/provided/cert", None), ("/path/to/provided/cert", "path/to/provided/client-cert"), ], ) def test_authenticator_uses_certs_from_config_if_not_provided( config: Config, mock_remote: responses.RequestsMock, mock_config: Config, http: responses.RequestsMock, mocker: MockerFixture, cert: str | None, client_cert: str | None, ) -> None: configured_cert = "/path/to/cert" configured_client_cert = "/path/to/client-cert" mock_config.merge( { "certificates": { "foo": {"cert": configured_cert, "client-cert": configured_client_cert} }, } ) authenticator = Authenticator(mock_config, NullIO()) url = "https://foo.bar/files/foo-0.1.0.tar.gz" session = authenticator.get_session(url) session_send = mocker.patch.object(session, "send") authenticator.request( "get", url, verify=cert, cert=client_cert, ) kwargs = session_send.call_args[1] assert Path(kwargs["verify"]) == Path(cert or configured_cert) assert Path(kwargs["cert"]) == Path(client_cert or configured_client_cert) def test_authenticator_uses_credentials_from_config_matched_by_url_path( config: Config, mock_remote: None, http: responses.RequestsMock ) -> None: config.merge( { "repositories": { "foo-alpha": {"url": "https://foo.bar/alpha/files/simple/"}, "foo-beta": {"url": "https://foo.bar/beta/files/simple/"}, }, "http-basic": { "foo-alpha": {"username": "bar", "password": "alpha"}, "foo-beta": {"username": "baz", "password": "beta"}, }, } ) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/alpha/files/simple/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"bar:alpha").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" # Make request on second repository with the same netloc but different credentials authenticator.request("get", "https://foo.bar/beta/files/simple/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"baz:beta").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_uses_credentials_from_config_with_at_sign_in_path( config: Config, mock_remote: None, http: responses.RequestsMock ) -> None: config.merge( { "repositories": { "foo": {"url": "https://foo.bar/beta/files/simple/"}, }, "http-basic": { "foo": {"username": "bar", "password": "baz"}, }, } ) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/beta/files/simple/f@@-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"bar:baz").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_falls_back_to_keyring_url_matched_by_path( config: Config, mock_remote: None, http: responses.RequestsMock, with_simple_keyring: None, dummy_keyring: DummyBackend, ) -> None: config.merge( { "repositories": { "foo-alpha": {"url": "https://foo.bar/alpha/files/simple/"}, "foo-beta": {"url": "https://foo.bar/beta/files/simple/"}, } } ) dummy_keyring.set_default_service_credential( "https://foo.bar/alpha/files/simple/", SimpleCredential("foo", "bar"), ) dummy_keyring.set_default_service_credential( "https://foo.bar/beta/files/simple/", SimpleCredential("foo", "baz"), ) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/alpha/files/simple/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"foo:bar").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" authenticator.request("get", "https://foo.bar/beta/files/simple/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"foo:baz").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_uses_env_provided_credentials_matched_by_url_path( config: Config, environ: None, mock_remote: responses.RequestsMock, http: responses.RequestsMock, monkeypatch: MonkeyPatch, ) -> None: monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_ALPHA_USERNAME", "bar") monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_ALPHA_PASSWORD", "alpha") monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_BETA_USERNAME", "baz") monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_BETA_PASSWORD", "beta") config.merge( { "repositories": { "foo-alpha": {"url": "https://foo.bar/alpha/files/simple/"}, "foo-beta": {"url": "https://foo.bar/beta/files/simple/"}, } } ) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/alpha/files/simple/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"bar:alpha").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" authenticator.request("get", "https://foo.bar/beta/files/simple/foo-0.1.0.tar.gz") request = http.calls[-1].request basic_auth = base64.b64encode(b"baz:beta").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_azure_feed_guid_credentials( config: Config, mock_remote: None, http: responses.RequestsMock, with_simple_keyring: None, dummy_keyring: DummyBackend, ) -> None: config.merge( { "repositories": { "alpha": { "url": "https://foo.bar/org-alpha/_packaging/feed/pypi/simple/" }, "beta": { "url": "https://foo.bar/org-beta/_packaging/feed/pypi/simple/" }, }, "http-basic": { "alpha": {"username": "foo", "password": "bar"}, "beta": {"username": "baz", "password": "qux"}, }, } ) authenticator = Authenticator(config, NullIO()) authenticator.request( "get", "https://foo.bar/org-alpha/_packaging/GUID/pypi/simple/a/1.0.0/a-1.0.0.whl", ) request = http.calls[-1].request basic_auth = base64.b64encode(b"foo:bar").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" authenticator.request( "get", "https://foo.bar/org-beta/_packaging/GUID/pypi/simple/b/1.0.0/a-1.0.0.whl", ) request = http.calls[-1].request basic_auth = base64.b64encode(b"baz:qux").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_add_repository( config: Config, mock_remote: None, http: responses.RequestsMock, with_simple_keyring: None, dummy_keyring: DummyBackend, ) -> None: config.merge( { "http-basic": { "source": {"username": "foo", "password": "bar"}, }, } ) authenticator = Authenticator(config, NullIO()) authenticator.request( "get", "https://foo.bar/simple/a/1.0.0/a-1.0.0.whl", ) request = http.calls[-1].request assert "Authorization" not in request.headers authenticator.add_repository("source", "https://foo.bar/simple/") authenticator.request( "get", "https://foo.bar/simple/a/1.0.0/a-1.0.0.whl", ) request = http.calls[-1].request basic_auth = base64.b64encode(b"foo:bar").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_authenticator_git_repositories( config: Config, mock_remote: None, http: responses.RequestsMock, with_simple_keyring: None, dummy_keyring: DummyBackend, ) -> None: config.merge( { "repositories": { "one": {"url": "https://foo.bar/org/one.git"}, "two": {"url": "https://foo.bar/org/two.git"}, }, "http-basic": { "one": {"username": "foo", "password": "bar"}, "two": {"username": "baz", "password": "qux"}, }, } ) authenticator = Authenticator(config, NullIO()) one = authenticator.get_credentials_for_git_url("https://foo.bar/org/one.git") assert one.username == "foo" assert one.password == "bar" two = authenticator.get_credentials_for_git_url("https://foo.bar/org/two.git") assert two.username == "baz" assert two.password == "qux" two_ssh = authenticator.get_credentials_for_git_url("ssh://git@foo.bar/org/two.git") assert not two_ssh.username assert not two_ssh.password three = authenticator.get_credentials_for_git_url("https://foo.bar/org/three.git") assert not three.username assert not three.password @pytest.mark.parametrize( ("ca_cert", "client_cert", "result"), [ (None, None, RepositoryCertificateConfig()), ( "path/to/ca.pem", "path/to/client.pem", RepositoryCertificateConfig( Path("path/to/ca.pem"), Path("path/to/client.pem") ), ), ( None, "path/to/client.pem", RepositoryCertificateConfig(None, Path("path/to/client.pem")), ), ( "path/to/ca.pem", None, RepositoryCertificateConfig(Path("path/to/ca.pem"), None), ), (True, None, RepositoryCertificateConfig()), (False, None, RepositoryCertificateConfig(verify=False)), ( False, "path/to/client.pem", RepositoryCertificateConfig(None, Path("path/to/client.pem"), verify=False), ), ], ) def test_repository_certificate_configuration_create( ca_cert: str | bool | None, client_cert: str | None, result: RepositoryCertificateConfig, config: Config, ) -> None: cert_config = {} if ca_cert is not None: cert_config["cert"] = ca_cert if client_cert is not None: cert_config["client-cert"] = client_cert config.merge({"certificates": {"foo": cert_config}}) assert RepositoryCertificateConfig.create("foo", config) == result ================================================ FILE: tests/utils/test_cache.py ================================================ from __future__ import annotations import concurrent.futures import shutil import traceback from pathlib import Path from typing import TYPE_CHECKING from typing import TypeVar import pytest from packaging.tags import Tag from poetry.core.packages.utils.link import Link from poetry.utils.cache import ArtifactCache from poetry.utils.cache import FileCache from poetry.utils.env import MockEnv if TYPE_CHECKING: from typing import Any from pytest_mock import MockerFixture from tests.conftest import Config from tests.types import FixtureDirGetter T = TypeVar("T") @pytest.fixture def repository_cache_dir(config: Config) -> Path: return config.repository_cache_directory @pytest.fixture def poetry_file_cache(repository_cache_dir: Path) -> FileCache[Any]: return FileCache(repository_cache_dir / "cache") def test_cache_validates(repository_cache_dir: Path) -> None: with pytest.raises(ValueError) as e: FileCache(repository_cache_dir / "cache", hash_type="unknown") assert str(e.value) == "FileCache.hash_type is unknown value: 'unknown'." def test_cache_get_put_has(repository_cache_dir: Path) -> None: cache: FileCache[Any] = FileCache(repository_cache_dir / "cache") cache.put("key1", "value") cache.put("key2", {"a": ["json-encoded", "value"]}) assert cache.get("key1") == "value" assert cache.get("key2") == {"a": ["json-encoded", "value"]} assert cache.has("key1") assert cache.has("key2") assert not cache.has("key3") def test_cache_forget(repository_cache_dir: Path) -> None: cache: FileCache[Any] = FileCache(repository_cache_dir / "cache") cache.put("key1", "value") cache.put("key2", "value") assert cache.has("key1") assert cache.has("key2") cache.forget("key1") assert not cache.has("key1") assert cache.has("key2") def test_cache_flush(repository_cache_dir: Path) -> None: cache: FileCache[Any] = FileCache(repository_cache_dir / "cache") cache.put("key1", "value") cache.put("key2", "value") assert cache.has("key1") assert cache.has("key2") cache.flush() assert not cache.has("key1") assert not cache.has("key2") def test_cache_remember(repository_cache_dir: Path, mocker: MockerFixture) -> None: cache: FileCache[Any] = FileCache(repository_cache_dir / "cache") method = mocker.Mock(return_value="value2") cache.put("key1", "value1") assert cache.remember("key1", method) == "value1" method.assert_not_called() assert cache.remember("key2", method) == "value2" method.assert_called() def test_cache_get_limited_minutes( repository_cache_dir: Path, mocker: MockerFixture ) -> None: cache: FileCache[Any] = FileCache(repository_cache_dir / "cache") start_time = 1111111111 mocker.patch("time.time", return_value=start_time) cache.put("key1", "value", minutes=5) cache.put("key2", "value", minutes=5) assert cache.get("key1") is not None assert cache.get("key2") is not None mocker.patch("time.time", return_value=start_time + 5 * 60 + 1) # check to make sure that the cache deletes for has() and get() assert not cache.has("key1") assert cache.get("key2") is None def test_missing_cache_file(poetry_file_cache: FileCache[Any]) -> None: poetry_file_cache.put("key1", "value") key1_path = ( poetry_file_cache.path / "81/74/09/96/87/a2/66/21/8174099687a26621f4e2cdd7cc03b3dacedb3fb962255b1aafd033cabe831530" ) assert key1_path.exists() key1_path.unlink() # corrupt cache by removing a key file assert poetry_file_cache.get("key1") is None def test_missing_cache_path(poetry_file_cache: FileCache[Any]) -> None: poetry_file_cache.put("key1", "value") key1_partial_path = poetry_file_cache.path / "81/74/09/96/87/a2/" assert key1_partial_path.exists() shutil.rmtree( key1_partial_path ) # corrupt cache by removing a subdirectory containing a key file assert poetry_file_cache.get("key1") is None @pytest.mark.parametrize( "corrupt_payload", [ "", # empty file b"\x00", # null "99999999", # truncated file '999999a999"value"', # corrupt lifetime b'9999999999"va\xd8\x00"', # invalid unicode "fil3systemFa!led", # garbage file ], ) def test_detect_corrupted_cache_key_file( corrupt_payload: str | bytes, poetry_file_cache: FileCache[Any] ) -> None: poetry_file_cache.put("key1", "value") key1_path = ( poetry_file_cache.path / "81/74/09/96/87/a2/66/21/8174099687a26621f4e2cdd7cc03b3dacedb3fb962255b1aafd033cabe831530" ) assert key1_path.exists() # original content: 9999999999"value" if isinstance(corrupt_payload, str): with open(key1_path, "w", encoding="utf-8") as f: f.write(corrupt_payload) # write corrupt data else: with open(key1_path, "wb") as f: f.write(corrupt_payload) # write corrupt data assert poetry_file_cache.get("key1") is None def test_get_cache_directory_for_link(tmp_path: Path) -> None: cache = ArtifactCache(cache_dir=tmp_path) directory = cache.get_cache_directory_for_link( Link("https://files.pythonhosted.org/poetry-1.1.0.tar.gz") ) expected = Path( f"{tmp_path.as_posix()}/41/9c/6e/" "ef83f08fcf4dac7cd78d843e7974d601a19c90e4bb90bb76b4a7a61548" ) assert directory == expected @pytest.mark.parametrize("subdirectory", [None, "subdir"]) def test_get_cache_directory_for_git(tmp_path: Path, subdirectory: str | None) -> None: cache = ArtifactCache(cache_dir=tmp_path) directory = cache.get_cache_directory_for_git( url="https://github.com/demo/demo.git", ref="123456", subdirectory=subdirectory ) if subdirectory: expected = Path( f"{tmp_path.as_posix()}/53/08/33/" "7851e5806669aa15ab0c555b13bd5523978057323c6a23a9cee18ec51c" ) else: expected = Path( f"{tmp_path.as_posix()}/61/14/30/" "7c57f8fd71e4eee40b18893b9b586cba45177f15e300f4fb8b14ccc933" ) assert directory == expected def test_get_cached_archives(fixture_dir: FixtureDirGetter) -> None: distributions = fixture_dir("distributions") cache = ArtifactCache(cache_dir=Path()) archives = cache._get_cached_archives(distributions) assert archives assert set(archives) == set(distributions.glob("*.whl")) | set( distributions.glob("*.tar.gz") ) @pytest.mark.parametrize( ("link", "strict", "available_packages"), [ ( "https://files.pythonhosted.org/demo-0.1.0.tar.gz", True, [ Path("/cache/demo-0.1.0-py2.py3-none-any"), Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), ], ), ( "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", False, [], ), ], ) def test_get_not_found_cached_archive_for_link( mocker: MockerFixture, link: str, strict: bool, available_packages: list[Path], ) -> None: env = MockEnv( version_info=(3, 8, 3), marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, supported_tags=[ Tag("cp38", "cp38", "macosx_10_15_x86_64"), Tag("py3", "none", "any"), ], ) cache = ArtifactCache(cache_dir=Path()) mocker.patch.object( cache, "_get_cached_archives", return_value=available_packages, ) archive = cache.get_cached_archive_for_link(Link(link), strict=strict, env=env) assert archive is None @pytest.mark.parametrize( ("link", "cached", "strict"), [ ( "https://files.pythonhosted.org/demo-0.1.0.tar.gz", "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", False, ), ( "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", False, ), ( "https://files.pythonhosted.org/demo-0.1.0.tar.gz", "/cache/demo-0.1.0.tar.gz", True, ), ( "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", True, ), ], ) def test_get_found_cached_archive_for_link( mocker: MockerFixture, link: str, cached: str, strict: bool, ) -> None: env = MockEnv( version_info=(3, 8, 3), marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, supported_tags=[ Tag("cp38", "cp38", "macosx_10_15_x86_64"), Tag("py3", "none", "any"), ], ) cache = ArtifactCache(cache_dir=Path()) mocker.patch.object( cache, "_get_cached_archives", return_value=[ Path("/cache/demo-0.1.0-py2.py3-none-any"), Path("/cache/demo-0.1.0.tar.gz"), Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), ], ) archive = cache.get_cached_archive_for_link(Link(link), strict=strict, env=env) assert Path(cached) == archive def test_get_cached_archive_for_link_no_race_condition( tmp_path: Path, mocker: MockerFixture ) -> None: cache = ArtifactCache(cache_dir=tmp_path) link = Link("https://files.pythonhosted.org/demo-0.1.0.tar.gz") def replace_file(_: str, dest: Path) -> None: dest.unlink(missing_ok=True) # write some data (so it takes a while) to provoke possible race conditions dest.write_text("a" * 2**20, encoding="utf-8") download_mock = mocker.Mock(side_effect=replace_file) def get_archive(link: Link) -> Path: path: Path = cache.get_cached_archive_for_link( link, strict=True, download_func=download_mock ) return path with concurrent.futures.ThreadPoolExecutor() as executor: tasks = [] for _ in range(4): tasks.append(executor.submit(get_archive, link)) concurrent.futures.wait(tasks) results = set() for task in tasks: try: results.add(task.result()) except Exception: pytest.fail(traceback.format_exc()) assert results == {cache.get_cache_directory_for_link(link) / link.filename} download_mock.assert_called_once() def test_get_cached_archive_for_git() -> None: """Smoke test that checks that no assertion is raised.""" cache = ArtifactCache(cache_dir=Path()) archive = cache.get_cached_archive_for_git("url", "ref", "subdirectory", MockEnv()) assert archive is None ================================================ FILE: tests/utils/test_dependency_specification.py ================================================ from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING import pytest from deepdiff.diff import DeepDiff from poetry.inspection.info import PackageInfo from poetry.utils.dependency_specification import RequirementsParser if TYPE_CHECKING: from collections.abc import Collection from pytest_mock import MockerFixture from poetry.utils.cache import ArtifactCache from poetry.utils.dependency_specification import DependencySpec @pytest.mark.parametrize( ("requirement", "expected_variants"), [ ( "git+http://github.com/demo/demo.git", ({"git": "http://github.com/demo/demo.git", "name": "demo"},), ), ( "git+https://github.com/demo/demo.git", ({"git": "https://github.com/demo/demo.git", "name": "demo"},), ), ( "git+ssh://github.com/demo/demo.git", ({"git": "ssh://github.com/demo/demo.git", "name": "demo"},), ), ( "git+https://github.com/demo/demo.git#main", ( { "git": "https://github.com/demo/demo.git", "name": "demo", "rev": "main", }, ), ), ( "git+https://github.com/demo/demo.git@main", ( { "git": "https://github.com/demo/demo.git", "name": "demo", "rev": "main", }, ), ), ( "git+https://github.com/demo/subdirectories.git@main#subdirectory=two", ( { "git": "https://github.com/demo/subdirectories.git", "name": "two", "rev": "main", "subdirectory": "two", }, ), ), ("demo", ({"name": "demo"},)), ("demo@1.0.0", ({"name": "demo", "version": "1.0.0"},)), ("demo@^1.0.0", ({"name": "demo", "version": "^1.0.0"},)), ("demo@==1.0.0", ({"name": "demo", "version": "==1.0.0"},)), ("demo@!=1.0.0", ({"name": "demo", "version": "!=1.0.0"},)), ("demo@~1.0.0", ({"name": "demo", "version": "~1.0.0"},)), ( "demo[a,b]@1.0.0", ({"name": "demo", "version": "1.0.0", "extras": ["a", "b"]},), ), ("demo[a,b]", ({"name": "demo", "extras": ["a", "b"]},)), ("../demo", ({"name": "demo", "path": "../demo"},)), ("../demo/demo.whl", ({"name": "demo", "path": "../demo/demo.whl"},)), ( "https://files.pythonhosted.org/distributions/demo-0.1.0.tar.gz", ( { "name": "demo", "url": "https://files.pythonhosted.org/distributions/demo-0.1.0.tar.gz", }, ), ), # PEP 508 inputs ( "poetry-core (>=1.0.7,<1.1.0)", ({"name": "poetry-core", "version": ">=1.0.7,<1.1.0"},), ), ( 'requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < "2.7"', ( # allow several equivalent versions to make test more robust { "name": "requests", "markers": 'python_version < "2.7"', "version": ">=2.8.1,<2.9", "extras": ["security", "tests"], }, { "name": "requests", "markers": 'python_version < "2.7"', "version": ">=2.8.1,<2.9.0", "extras": ["security", "tests"], }, { "name": "requests", "markers": 'python_version < "2.7"', "version": ">=2.8.1,<2.9.dev0", "extras": ["security", "tests"], }, { "name": "requests", "markers": 'python_version < "2.7"', "version": ">=2.8.1,<2.9.0.dev0", "extras": ["security", "tests"], }, { "name": "requests", "markers": 'python_version < "2.7"', "version": ">=2.8.1,==2.8.*", "extras": ["security", "tests"], }, ), ), ("name (>=3,<4)", ({"name": "name", "version": ">=3,<4"},)), ( "name@http://foo.com", ({"name": "name", "url": "http://foo.com"},), ), ( "name [fred,bar] @ http://foo.com ; python_version=='2.7'", ( { "name": "name", "markers": 'python_version == "2.7"', "url": "http://foo.com", "extras": ["fred", "bar"], }, ), ), ( ( 'cachecontrol[filecache] (>=0.12.9,<0.13.0); python_version >= "3.6"' ' and python_version < "4.0"' ), ( { "version": ">=0.12.9,<0.13.0", "markers": 'python_version >= "3.6" and python_version < "4.0"', "extras": ["filecache"], "name": "cachecontrol", }, ), ), ], ) def test_parse_dependency_specification( requirement: str, expected_variants: Collection[DependencySpec], mocker: MockerFixture, artifact_cache: ArtifactCache, ) -> None: original = Path.exists # Parsing file and path dependencies reads metadata from the file or path in # question: for these tests we mock that out. def _mock(self: Path) -> bool: if "/" in requirement and self == Path.cwd().joinpath(requirement): return True return original(self) mocker.patch("pathlib.Path.exists", _mock) mocker.patch( "poetry.inspection.info.get_pep517_metadata", return_value=PackageInfo(name="demo", version="0.1.2"), ) assert any( not DeepDiff( RequirementsParser(artifact_cache=artifact_cache).parse(requirement), specification, ignore_order=True, ) for specification in expected_variants ) ================================================ FILE: tests/utils/test_extras.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import pytest from poetry.core.packages.package import Package from poetry.factory import Factory from poetry.utils.extras import get_extra_package_names if TYPE_CHECKING: from packaging.utils import NormalizedName _PACKAGE_FOO = Package("foo", "0.1.0") _PACKAGE_SPAM = Package("spam", "0.2.0") _PACKAGE_BAR = Package("bar", "0.3.0") _PACKAGE_BAR.add_dependency(Factory.create_dependency("foo", "*")) # recursive dependency _PACKAGE_BAZ = Package("baz", "0.4.0") _PACKAGE_BAZ.add_dependency(Factory.create_dependency("quix", "*")) _PACKAGE_QUIX = Package("quix", "0.5.0") _PACKAGE_QUIX.add_dependency(Factory.create_dependency("baz", "*")) @pytest.mark.parametrize( ["packages", "extras", "extra_names", "expected_extra_package_names"], [ # Empty edge case ([], {}, [], set()), # Selecting no extras is fine ([_PACKAGE_FOO], {}, [], set()), # An empty extras group should return an empty list ([_PACKAGE_FOO], {"group0": []}, ["group0"], set()), # Selecting an extras group should return the contained packages ( [_PACKAGE_FOO, _PACKAGE_SPAM, _PACKAGE_BAR], {"group0": ["foo"]}, ["group0"], {"foo"}, ), # If a package has dependencies, we should also get their names ( [_PACKAGE_FOO, _PACKAGE_SPAM, _PACKAGE_BAR], {"group0": ["bar"], "group1": ["spam"]}, ["group0"], {"bar", "foo"}, ), # Selecting multiple extras should get us the union of all package names ( [_PACKAGE_FOO, _PACKAGE_SPAM, _PACKAGE_BAR], {"group0": ["bar"], "group1": ["spam"]}, ["group0", "group1"], {"bar", "foo", "spam"}, ), ( [_PACKAGE_BAZ, _PACKAGE_QUIX], {"group0": ["baz"], "group1": ["quix"]}, ["group0", "group1"], {"baz", "quix"}, ), ], ) def test_get_extra_package_names( packages: list[Package], extras: dict[NormalizedName, list[NormalizedName]], extra_names: list[NormalizedName], expected_extra_package_names: set[str], ) -> None: assert ( get_extra_package_names(packages, extras, extra_names) == expected_extra_package_names ) ================================================ FILE: tests/utils/test_helpers.py ================================================ from __future__ import annotations import base64 import re from pathlib import Path from typing import TYPE_CHECKING from typing import Any import pytest import responses from poetry.core.utils.helpers import parse_requires from requests.exceptions import ChunkedEncodingError from poetry.utils.helpers import Downloader from poetry.utils.helpers import HTTPRangeRequestSupportedError from poetry.utils.helpers import download_file from poetry.utils.helpers import ensure_path from poetry.utils.helpers import get_file_hash from poetry.utils.helpers import get_highest_priority_hash_type if TYPE_CHECKING: from requests import PreparedRequest from tests.conftest import Config from tests.types import FixtureDirGetter from tests.types import HttpResponse def test_parse_requires() -> None: requires = """\ jsonschema>=2.6.0.0,<3.0.0.0 lockfile>=0.12.0.0,<0.13.0.0 pip-tools>=1.11.0.0,<2.0.0.0 pkginfo>=1.4.0.0,<2.0.0.0 pyrsistent>=0.14.2.0,<0.15.0.0 toml>=0.9.0.0,<0.10.0.0 cleo>=0.6.0.0,<0.7.0.0 cachy>=0.1.1.0,<0.2.0.0 cachecontrol>=0.12.4.0,<0.13.0.0 requests>=2.18.0.0,<3.0.0.0 msgpack-python>=0.5.0.0,<0.6.0.0 pyparsing>=2.2.0.0,<3.0.0.0 requests-toolbelt>=0.8.0.0,<0.9.0.0 [:(python_version >= "2.7.0.0" and python_version < "2.8.0.0")\ or (python_version >= "3.4.0.0" and python_version < "3.5.0.0")] typing>=3.6.0.0,<4.0.0.0 [:python_version >= "2.7.0.0" and python_version < "2.8.0.0"] virtualenv>=15.2.0.0,<16.0.0.0 pathlib2>=2.3.0.0,<3.0.0.0 [:python_version >= "3.4.0.0" and python_version < "3.6.0.0"] zipfile36>=0.1.0.0,<0.2.0.0 [dev] isort@ git+git://github.com/timothycrosley/isort.git@e63ae06ec7d70b06df9e528357650281a3d3ec22#egg=isort """ result = parse_requires(requires) # fmt: off expected = [ "jsonschema>=2.6.0.0,<3.0.0.0", "lockfile>=0.12.0.0,<0.13.0.0", "pip-tools>=1.11.0.0,<2.0.0.0", "pkginfo>=1.4.0.0,<2.0.0.0", "pyrsistent>=0.14.2.0,<0.15.0.0", "toml>=0.9.0.0,<0.10.0.0", "cleo>=0.6.0.0,<0.7.0.0", "cachy>=0.1.1.0,<0.2.0.0", "cachecontrol>=0.12.4.0,<0.13.0.0", "requests>=2.18.0.0,<3.0.0.0", "msgpack-python>=0.5.0.0,<0.6.0.0", "pyparsing>=2.2.0.0,<3.0.0.0", "requests-toolbelt>=0.8.0.0,<0.9.0.0", 'typing>=3.6.0.0,<4.0.0.0 ; (python_version >= "2.7.0.0" and python_version < "2.8.0.0") or (python_version >= "3.4.0.0" and python_version < "3.5.0.0")', 'virtualenv>=15.2.0.0,<16.0.0.0 ; python_version >= "2.7.0.0" and python_version < "2.8.0.0"', 'pathlib2>=2.3.0.0,<3.0.0.0 ; python_version >= "2.7.0.0" and python_version < "2.8.0.0"', 'zipfile36>=0.1.0.0,<0.2.0.0 ; python_version >= "3.4.0.0" and python_version < "3.6.0.0"', 'isort@ git+git://github.com/timothycrosley/isort.git@e63ae06ec7d70b06df9e528357650281a3d3ec22#egg=isort ; extra == "dev"', ] # fmt: on assert result == expected def test_default_hash(fixture_dir: FixtureDirGetter) -> None: sha_256 = "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" assert get_file_hash(fixture_dir("distributions") / "demo-0.1.0.tar.gz") == sha_256 @pytest.mark.parametrize( "hash_name,expected", [ ("sha224", "d26bd24163fe91c16b4b0162e773514beab77b76114d9faf6a31e350"), ( "sha3_512", "196f4af9099185054ed72ca1d4c57707da5d724df0af7c3dfcc0fd018b0e0533908e790a291600c7d196fe4411b4f5f6db45213fe6e5cd5512bf18b2e9eff728", ), ( "blake2s", "6dd9007d36c106defcf362cc637abeca41e8e93999928c8fcfaba515ed33bc93", ), ( "sha3_384", "787264d7885a0c305d2ee4daecfff435d11818399ef96cacef7e7c6bb638ce475f630d39fdd2800ca187dcd0071dc410", ), ( "blake2b", "077a34e8252c8f6776bddd0d34f321cc52762cb4c11a1c7aa9b6168023f1722caf53c9f029074a6eb990a8de341d415dd986293bc2a2fccddad428be5605696e", ), ( "sha256", "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", ), ( "sha512", "766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad", ), ( "sha384", "c638f32460f318035e4600284ba64fb531630740aebd33885946e527002d742787ff09eb65fd81bc34ce5ff5ef11cfe8", ), ("sha3_224", "72980fc7bdf8c4d34268dc469442b09e1ccd2a8ff390954fc4d55a5a"), ("sha1", "91b585bd38f72d7ceedb07d03f94911b772fdc4c"), ( "sha3_256", "7da5c08b416e6bcb339d6bedc0fe077c6e69af00607251ef4424c356ea061fcb", ), ], ) def test_guaranteed_hash( hash_name: str, expected: str, fixture_dir: FixtureDirGetter ) -> None: file_path = fixture_dir("distributions") / "demo-0.1.0.tar.gz" assert get_file_hash(file_path, hash_name) == expected def test_download_file( http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: file_path = fixture_dir("distributions") / "demo-0.1.0.tar.gz" url = "https://foo.com/demo-0.1.0.tar.gz" http.get(url, body=file_path.read_bytes()) dest = tmp_path / "demo-0.1.0.tar.gz" download_file(url, dest) expect_sha_256 = "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" assert get_file_hash(dest) == expect_sha_256 assert http.calls[-1].request.headers["Accept-Encoding"] == "Identity" def test_download_file_recover_from_error( http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: file_path = fixture_dir("distributions") / "demo-0.1.0.tar.gz" file_body = file_path.read_bytes() file_length = len(file_body) url = "https://foo.com/demo-0.1.0.tar.gz" def handle_request(request: PreparedRequest) -> HttpResponse: if request.headers.get("Range") is None: response_headers = { "Content-Length": str(file_length), "Accept-Ranges": "bytes", } return 200, response_headers, file_body[: file_length // 2] else: start = int( request.headers.get("Range", "bytes=0-").split("=")[1].split("-")[0] ) response_headers = {"Content-Length": str(len(file_body[start:]))} return 206, response_headers, file_body[start:] http.add_callback(responses.GET, url, callback=handle_request) dest = tmp_path / "demo-0.1.0.tar.gz" download_file(url, dest, chunk_size=file_length // 2, max_retries=1) expect_sha_256 = "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" assert get_file_hash(dest) == expect_sha_256 assert http.calls[-1].request.headers["Accept-Encoding"] == "Identity" assert http.calls[-1].request.headers["Range"] == f"bytes={file_length // 2}-" def test_download_file_fail_when_no_range( http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: file_path = fixture_dir("distributions") / "demo-0.1.0.tar.gz" file_body = file_path.read_bytes() file_length = len(file_body) url = "https://foo.com/demo-0.1.0.tar.gz" def handle_request(request: PreparedRequest) -> HttpResponse: response_headers = {"Content-Length": str(file_length)} return 200, response_headers, file_body[: file_length // 2] http.add_callback(responses.GET, url, callback=handle_request) dest = tmp_path / "demo-0.1.0.tar.gz" with pytest.raises(ChunkedEncodingError): download_file(url, dest, chunk_size=file_length // 2, max_retries=1) def test_download_file_fail_when_first_chunk_failed( http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: file_path = fixture_dir("distributions") / "demo-0.1.0.tar.gz" file_body = file_path.read_bytes() file_length = len(file_body) url = "https://foo.com/demo-0.1.0.tar.gz" def handle_request(request: PreparedRequest) -> tuple[int, dict[str, Any], bytes]: response_headers = { "Content-Length": str(file_length), "Accept-Ranges": "bytes", } return 200, response_headers, file_body[: file_length // 2] http.add_callback(responses.GET, url, callback=handle_request) dest = tmp_path / "demo-0.1.0.tar.gz" with pytest.raises(ChunkedEncodingError): download_file(url, dest, chunk_size=file_length, max_retries=1) @pytest.mark.parametrize( "hash_types,expected", [ (("sha512", "sha3_512", "md5"), "sha3_512"), ("md5", "md5"), (("blah", "blah_blah"), None), ((), None), ], ) def test_highest_priority_hash_type(hash_types: set[str], expected: str | None) -> None: assert get_highest_priority_hash_type(hash_types, "Blah") == expected @pytest.mark.parametrize("accepts_ranges", [False, True]) @pytest.mark.parametrize("raise_accepts_ranges", [False, True]) def test_download_file_raise_accepts_ranges( http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path, accepts_ranges: bool, raise_accepts_ranges: bool, ) -> None: filename = "demo-0.1.0-py2.py3-none-any.whl" def handle_request(request: PreparedRequest) -> tuple[int, dict[str, Any], bytes]: file_path = fixture_dir("distributions") / filename response_headers = {} if accepts_ranges: response_headers["Accept-Ranges"] = "bytes" return 200, response_headers, file_path.read_bytes() url = f"https://foo.com/{filename}" http.add_callback(responses.GET, url, callback=handle_request) dest = tmp_path / filename if accepts_ranges and raise_accepts_ranges: with pytest.raises(HTTPRangeRequestSupportedError): download_file(url, dest, raise_accepts_ranges=raise_accepts_ranges) assert not dest.exists() else: download_file(url, dest, raise_accepts_ranges=raise_accepts_ranges) assert dest.is_file() def test_downloader_uses_authenticator_by_default( config: Config, http: responses.RequestsMock, tmp_working_directory: Path, ) -> None: import poetry.utils.authenticator # force set default authenticator to None so that it is recreated using patched config poetry.utils.authenticator._authenticator = None config.merge( { "repositories": {"foo": {"url": "https://foo.bar/files/"}}, "http-basic": {"foo": {"username": "bar", "password": "baz"}}, } ) http.get( re.compile("^https?://foo.bar/(.+?)$"), ) Downloader( "https://foo.bar/files/foo-0.1.0.tar.gz", tmp_working_directory / "foo-0.1.0.tar.gz", ) request = http.calls[-1].request basic_auth = base64.b64encode(b"bar:baz").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" def test_ensure_path_converts_string(tmp_path: Path) -> None: assert tmp_path.exists() assert ensure_path(path=tmp_path.as_posix(), is_directory=True) == tmp_path def test_ensure_path_does_not_convert_path(tmp_path: Path) -> None: assert tmp_path.exists() assert Path(tmp_path.as_posix()) is not tmp_path result = ensure_path(path=tmp_path, is_directory=True) assert result == tmp_path assert result is tmp_path def test_ensure_path_is_directory_parameter(tmp_path: Path) -> None: with pytest.raises(ValueError): ensure_path(path=tmp_path, is_directory=False) assert ensure_path(path=tmp_path, is_directory=True) is tmp_path def test_ensure_path_file(tmp_path: Path) -> None: path = tmp_path.joinpath("some_file.txt") assert not path.exists() with pytest.raises(ValueError): ensure_path(path=path, is_directory=False) path.write_text("foobar", encoding="utf-8") assert ensure_path(path=path, is_directory=False) is path def test_ensure_path_directory(tmp_path: Path) -> None: path = tmp_path.joinpath("foobar") assert not path.exists() with pytest.raises(ValueError): ensure_path(path=path, is_directory=True) path.mkdir() assert ensure_path(path=path, is_directory=True) is path ================================================ FILE: tests/utils/test_isolated_build.py ================================================ from __future__ import annotations import shutil import sys import uuid from pathlib import Path from typing import TYPE_CHECKING import pytest from poetry.core.packages.dependency import Dependency from poetry.factory import Factory from poetry.puzzle.exceptions import SolverProblemError from poetry.puzzle.provider import IncompatibleConstraintsError from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository from poetry.utils.env import ephemeral_environment from poetry.utils.isolated_build import CONSTRAINTS_GROUP_NAME from poetry.utils.isolated_build import IsolatedBuildInstallError from poetry.utils.isolated_build import IsolatedEnv from poetry.utils.isolated_build import isolated_builder from tests.helpers import get_dependency if TYPE_CHECKING: from collections.abc import Collection from pytest_mock import MockerFixture from poetry.repositories.pypi_repository import PyPiRepository from tests.types import FixtureDirGetter @pytest.fixture() def pool(pypi_repository: PyPiRepository) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(pypi_repository) return pool @pytest.fixture(autouse=True) def setup(mocker: MockerFixture, pool: RepositoryPool) -> None: mocker.patch.object(Factory, "create_pool", return_value=pool) def test_isolated_env_install_success(pool: RepositoryPool) -> None: with ephemeral_environment(Path(sys.executable)) as venv: env = IsolatedEnv(venv, pool) assert not InstalledRepository.load(venv).find_packages( get_dependency("poetry-core") ) env.install({"poetry-core"}) assert InstalledRepository.load(venv).find_packages( get_dependency("poetry-core") ) def test_isolated_env_install_with_constraints_success(pool: RepositoryPool) -> None: constraints = [ Dependency("poetry-core", "<2", groups=[CONSTRAINTS_GROUP_NAME]), Dependency("attrs", ">1", groups=[CONSTRAINTS_GROUP_NAME]), ] with ephemeral_environment(Path(sys.executable)) as venv: env = IsolatedEnv(venv, pool) assert not InstalledRepository.load(venv).find_packages( get_dependency("poetry-core") ) assert not InstalledRepository.load(venv).find_packages(get_dependency("attrs")) env.install({"poetry-core"}, constraints=constraints) assert InstalledRepository.load(venv).find_packages( get_dependency("poetry-core") ) assert not InstalledRepository.load(venv).find_packages(get_dependency("attrs")) def test_isolated_env_install_discards_requirements_not_needed_by_env( pool: RepositoryPool, ) -> None: with ephemeral_environment(Path(sys.executable)) as venv: env = IsolatedEnv(venv, pool) assert not InstalledRepository.load(venv).find_packages( get_dependency("poetry-core") ) venv_python_version = venv.get_marker_env().get("python_version") package_one = uuid.uuid4().hex package_two = uuid.uuid4().hex env.install( { f"poetry-core; python_version=='{venv_python_version}'", f"{package_one}>=1.0.0; python_version=='0.0'", f"{package_two}>=2.0.0; platform_system=='Mirrors'", } ) assert InstalledRepository.load(venv).find_packages( get_dependency("poetry-core") ) assert not InstalledRepository.load(venv).find_packages( get_dependency(package_one) ) assert not InstalledRepository.load(venv).find_packages( get_dependency(package_two) ) @pytest.mark.parametrize( ("requirements", "exception"), [ ({"poetry-core==1.5.0", "poetry-core==1.6.0"}, IncompatibleConstraintsError), ({"black==19.10b0", "attrs==17.4.0"}, SolverProblemError), ], ) def test_isolated_env_install_error( requirements: Collection[str], exception: type[Exception], pool: RepositoryPool ) -> None: with ephemeral_environment(Path(sys.executable)) as venv: env = IsolatedEnv(venv, pool) with pytest.raises(exception): env.install(requirements) @pytest.mark.parametrize( ("requirements", "constraints", "exception"), [ ( {"poetry-core==1.5.0"}, [("poetry-core", "1.6.0")], IncompatibleConstraintsError, ), ({"black==19.10b0"}, [("attrs", "17.4.0")], SolverProblemError), ], ) def test_isolated_env_install_with_constraints_error( requirements: Collection[str], constraints: list[tuple[str, str]], exception: type[Exception], pool: RepositoryPool, ) -> None: with ephemeral_environment(Path(sys.executable)) as venv: env = IsolatedEnv(venv, pool) with pytest.raises(exception): env.install( requirements, constraints=[ Dependency(name, version, groups=[CONSTRAINTS_GROUP_NAME]) for name, version in constraints ], ) def test_isolated_env_install_failure( pool: RepositoryPool, mocker: MockerFixture ) -> None: mocker.patch("poetry.installation.installer.Installer.run", return_value=1) with ephemeral_environment(Path(sys.executable)) as venv: env = IsolatedEnv(venv, pool) with pytest.raises(IsolatedBuildInstallError) as e: env.install({"a", "b>1"}) assert e.value.requirements == {"a", "b>1"} def test_isolated_builder_outside_poetry_project_context( tmp_working_directory: Path, fixture_dir: FixtureDirGetter ) -> None: source = tmp_working_directory / "source" shutil.copytree(fixture_dir("project_with_setup"), source) destination = tmp_working_directory / "dist" try: with isolated_builder(source, "wheel") as builder: builder.metadata_path(destination) except RuntimeError: pytest.fail("Isolated builder did not fallback to default repository pool") ================================================ FILE: tests/utils/test_log_utils.py ================================================ from __future__ import annotations from poetry.core.packages.package import Package from poetry.utils.env.mock_env import MockEnv from poetry.utils.log_utils import format_build_wheel_log def test_format_build_wheel_log() -> None: env = MockEnv(version_info=(3, 13, 1), platform="win32", platform_machine="AMD64") package = Package(name="demo", version="1.2.3") result = format_build_wheel_log(package, env) expected = ( " Building a wheel file for demo for Python 3.13.1 on win32-AMD64" ) assert result == expected ================================================ FILE: tests/utils/test_password_manager.py ================================================ from __future__ import annotations import logging import os from typing import TYPE_CHECKING from unittest.mock import MagicMock import pytest from poetry.utils.password_manager import HTTPAuthCredential from poetry.utils.password_manager import PasswordManager from poetry.utils.password_manager import PoetryKeyring from poetry.utils.password_manager import PoetryKeyringError if TYPE_CHECKING: from pytest import LogCaptureFixture from pytest_mock import MockerFixture from tests.conftest import Config from tests.conftest import DummyBackend def test_set_http_password( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend ) -> None: manager = PasswordManager(config) assert PoetryKeyring.is_available() manager.set_http_password("foo", "bar", "baz") assert dummy_keyring.get_password("poetry-repository-foo", "bar") == "baz" auth = config.get("http-basic.foo") assert auth["username"] == "bar" assert "password" not in auth @pytest.mark.parametrize( ("username", "password", "is_valid"), [ ("bar", "baz", True), ("", "baz", True), ("bar", "", True), ("", "", False), ], ) def test_get_http_auth( username: str, password: str, is_valid: bool, config: Config, with_simple_keyring: None, poetry_keyring: PoetryKeyring, ) -> None: poetry_keyring.set_password("foo", username, password) config.auth_config_source.add_property("http-basic.foo", {"username": username}) manager = PasswordManager(config) assert PoetryKeyring.is_available() auth = manager.get_http_auth("foo") if is_valid: assert auth is not None assert auth.username == (username or None) assert auth.password == (password or None) else: assert auth.username is auth.password is None def test_delete_http_password( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend ) -> None: dummy_keyring.set_password("poetry-repository-foo", "bar", "baz") config.auth_config_source.add_property("http-basic.foo", {"username": "bar"}) manager = PasswordManager(config) assert PoetryKeyring.is_available() manager.delete_http_password("foo") assert dummy_keyring.get_password("poetry-repository-foo", "bar") is None assert config.get("http-basic.foo") is None def test_set_pypi_token( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend ) -> None: manager = PasswordManager(config) assert PoetryKeyring.is_available() manager.set_pypi_token("foo", "baz") assert config.get("pypi-token.foo") is None assert dummy_keyring.get_password("poetry-repository-foo", "__token__") == "baz" def test_get_pypi_token( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend ) -> None: dummy_keyring.set_password("poetry-repository-foo", "__token__", "baz") manager = PasswordManager(config) assert PoetryKeyring.is_available() assert manager.get_pypi_token("foo") == "baz" def test_delete_pypi_token( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend ) -> None: dummy_keyring.set_password("poetry-repository-foo", "__token__", "baz") manager = PasswordManager(config) assert PoetryKeyring.is_available() manager.delete_pypi_token("foo") assert dummy_keyring.get_password("poetry-repository-foo", "__token__") is None def test_set_http_password_with_unavailable_backend( config: Config, with_fail_keyring: None ) -> None: manager = PasswordManager(config) assert not PoetryKeyring.is_available() manager.set_http_password("foo", "bar", "baz") auth = config.get("http-basic.foo") assert auth["username"] == "bar" assert auth["password"] == "baz" @pytest.mark.parametrize( ("username", "password", "is_valid"), [ ("bar", "baz", True), ("", "baz", True), ("bar", "", True), ("", "", False), ], ) def test_get_http_auth_with_unavailable_backend( username: str, password: str, is_valid: bool, config: Config, with_fail_keyring: None, ) -> None: config.auth_config_source.add_property( "http-basic.foo", {"username": username, "password": password} ) manager = PasswordManager(config) assert not PoetryKeyring.is_available() auth = manager.get_http_auth("foo") if is_valid: assert auth is not None assert auth.username == (username or None) assert auth.password == (password or None) else: assert auth.username is auth.password is None def test_delete_http_password_with_unavailable_backend( config: Config, with_fail_keyring: None ) -> None: config.auth_config_source.add_property( "http-basic.foo", {"username": "bar", "password": "baz"} ) manager = PasswordManager(config) assert not PoetryKeyring.is_available() manager.delete_http_password("foo") assert config.get("http-basic.foo") is None def test_set_pypi_token_with_unavailable_backend( config: Config, with_fail_keyring: None ) -> None: manager = PasswordManager(config) assert not PoetryKeyring.is_available() manager.set_pypi_token("foo", "baz") assert config.get("pypi-token.foo") == "baz" def test_get_pypi_token_with_unavailable_backend( config: Config, with_fail_keyring: None ) -> None: config.auth_config_source.add_property("pypi-token.foo", "baz") manager = PasswordManager(config) assert not PoetryKeyring.is_available() assert manager.get_pypi_token("foo") == "baz" def test_delete_pypi_token_with_unavailable_backend( config: Config, with_fail_keyring: None ) -> None: config.auth_config_source.add_property("pypi-token.foo", "baz") manager = PasswordManager(config) assert not PoetryKeyring.is_available() manager.delete_pypi_token("foo") assert config.get("pypi-token.foo") is None def test_keyring_raises_errors_on_keyring_errors( mocker: MockerFixture, with_fail_keyring: None ) -> None: mocker.patch("poetry.utils.password_manager.PoetryKeyring.is_available") key_ring = PoetryKeyring("poetry") with pytest.raises(PoetryKeyringError): key_ring.set_password("foo", "bar", "baz") with pytest.raises(PoetryKeyringError): key_ring.get_password("foo", "bar") with pytest.raises(PoetryKeyringError): key_ring.delete_password("foo", "bar") def test_keyring_returns_none_on_locked_keyring( with_locked_keyring: None, caplog: LogCaptureFixture, ) -> None: caplog.set_level(logging.DEBUG, logger="poetry.utils.password_manager") key_ring = PoetryKeyring("poetry") cred = key_ring.get_credential("foo") assert cred.password is None assert "Keyring foo is locked" in caplog.messages def test_keyring_returns_none_on_erroneous_keyring( with_erroneous_keyring: None, caplog: LogCaptureFixture, ) -> None: caplog.set_level(logging.DEBUG, logger="poetry.utils.password_manager") key_ring = PoetryKeyring("poetry") cred = key_ring.get_credential("foo") assert cred.password is None assert "Accessing keyring foo failed" in caplog.messages def test_keyring_with_chainer_backend_and_fail_keyring_should_be_unavailable( with_chained_fail_keyring: None, ) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() def test_keyring_with_chainer_backend_and_null_keyring_should_be_unavailable( with_chained_null_keyring: None, ) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() def test_null_keyring_should_be_unavailable( with_null_keyring: None, ) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() def test_fail_keyring_should_be_unavailable( with_fail_keyring: None, ) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() def test_locked_keyring_should_not_be_available(with_locked_keyring: None) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() def test_erroneous_keyring_should_not_be_available( with_erroneous_keyring: None, ) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() def test_get_http_auth_from_environment_variables( environ: None, config: Config ) -> None: os.environ["POETRY_HTTP_BASIC_FOO_USERNAME"] = "bar" os.environ["POETRY_HTTP_BASIC_FOO_PASSWORD"] = "baz" manager = PasswordManager(config) auth = manager.get_http_auth("foo") assert auth == HTTPAuthCredential(username="bar", password="baz") def test_get_http_auth_does_not_call_keyring_when_credentials_in_environment_variables( environ: None, config: Config ) -> None: os.environ["POETRY_HTTP_BASIC_FOO_USERNAME"] = "bar" os.environ["POETRY_HTTP_BASIC_FOO_PASSWORD"] = "baz" manager = PasswordManager(config) manager.keyring = MagicMock() auth = manager.get_http_auth("foo") assert auth == HTTPAuthCredential(username="bar", password="baz") manager.keyring.get_password.assert_not_called() def test_get_http_auth_does_not_call_keyring_when_password_in_environment_variables( environ: None, config: Config ) -> None: config.merge( { "http-basic": {"foo": {"username": "bar"}}, } ) os.environ["POETRY_HTTP_BASIC_FOO_PASSWORD"] = "baz" manager = PasswordManager(config) manager.keyring = MagicMock() auth = manager.get_http_auth("foo") assert auth == HTTPAuthCredential(username="bar", password="baz") manager.keyring.get_password.assert_not_called() def test_get_pypi_token_with_env_var_positive( mocker: MockerFixture, config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend, ) -> None: sample_token = "sampletoken-1234" repo_name = "foo" manager = PasswordManager(config) mocker.patch.dict( os.environ, {f"POETRY_PYPI_TOKEN_{repo_name.upper()}": sample_token}, ) assert manager.get_pypi_token(repo_name) == sample_token def test_get_pypi_token_with_env_var_not_available( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend ) -> None: repo_name = "foo" manager = PasswordManager(config) result_token = manager.get_pypi_token(repo_name) assert result_token is None def test_disabled_keyring_never_called( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend ) -> None: config.config["keyring"]["enabled"] = False config.config["http-basic"] = {"onlyuser": {"username": "user"}} manager = PasswordManager(config) num_public_functions = len([f for f in dir(manager) if not f.startswith("_")]) if num_public_functions != 10: pytest.fail( f"A function was added to or removed from the {PasswordManager.__name__} " "class without reflecting this change in this test." ) with pytest.raises(PoetryKeyringError) as e: _ = manager.keyring assert str(e.value) == "Access to keyring was requested, but it is not available" # We made sure that accessing a disabled keyring raises an exception. # Now we call the PasswordManager functions that do access the keyring to # make sure that they never do so when the keyring is disabled. manager.set_pypi_token(repo_name="exists", token="token") manager.get_pypi_token(repo_name="exists") manager.get_pypi_token(repo_name="doesn't exist") manager.delete_pypi_token(repo_name="exists") manager.delete_pypi_token(repo_name="doesn't exist") manager.set_http_password(repo_name="exists", username="user", password="password") manager.get_http_auth(repo_name="exists") manager.get_http_auth(repo_name="doesn't exist") manager.get_http_auth(repo_name="onlyuser") manager.delete_http_password(repo_name="exits") manager.delete_http_password(repo_name="doesn't exist") manager.delete_http_password(repo_name="onlyuser") manager.get_credential("a", "b", "c", username="user") ================================================ FILE: tests/utils/test_patterns.py ================================================ from __future__ import annotations import pytest from poetry.utils import patterns @pytest.mark.parametrize( ["filename", "expected"], [ ( "markdown_captions-2-py3-none-any.whl", { "namever": "markdown_captions-2", "name": "markdown_captions", "ver": "2", "build": None, "pyver": "py3", "abi": "none", "plat": "any", }, ), ( "SQLAlchemy-1.3.20-cp27-cp27mu-manylinux2010_x86_64.whl", { "namever": "SQLAlchemy-1.3.20", "name": "SQLAlchemy", "ver": "1.3.20", "build": None, "pyver": "cp27", "abi": "cp27mu", "plat": "manylinux2010_x86_64", }, ), ( "isort-metadata-4.3.4-py2-none-any.whl", { "namever": "isort-metadata-4.3.4", "name": "isort-metadata", "ver": "4.3.4", "build": None, "pyver": "py2", "abi": "none", "plat": "any", }, ), ], ) def test_wheel_file_re(filename: str, expected: dict[str, str | None]) -> None: match = patterns.wheel_file_re.match(filename) assert match is not None groups = match.groupdict() assert groups == expected @pytest.mark.parametrize( ["filename", "expected"], [ ( "poetry_core-1.5.0.tar.gz", { "namever": "poetry_core-1.5.0", "name": "poetry_core", "ver": "1.5.0", "format": "tar.gz", }, ), ( "flask-restful-swagger-2-0.35.tar.gz", { "namever": "flask-restful-swagger-2-0.35", "name": "flask-restful-swagger-2", "ver": "0.35", "format": "tar.gz", }, ), ], ) def test_sdist_file_re(filename: str, expected: dict[str, str | None]) -> None: match = patterns.sdist_file_re.match(filename) assert match is not None groups = match.groupdict() assert groups == expected ================================================ FILE: tests/utils/test_pip.py ================================================ from __future__ import annotations import subprocess from typing import TYPE_CHECKING import pytest from poetry.utils.pip import pip_install if TYPE_CHECKING: from pathlib import Path from pytest_mock import MockerFixture from poetry.utils.env import VirtualEnv from tests.types import FixtureDirGetter def test_pip_install_successful( tmp_path: Path, tmp_venv: VirtualEnv, fixture_dir: FixtureDirGetter ) -> None: file_path = fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") result = pip_install(file_path, tmp_venv) assert "Successfully installed demo-0.1.0" in result def test_pip_install_with_keyboard_interrupt( tmp_path: Path, tmp_venv: VirtualEnv, fixture_dir: FixtureDirGetter, mocker: MockerFixture, ) -> None: file_path = fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") mocker.patch("subprocess.run", side_effect=KeyboardInterrupt()) with pytest.raises(KeyboardInterrupt): pip_install(file_path, tmp_venv) subprocess.run.assert_called_once() # type: ignore[attr-defined] ================================================ FILE: tests/utils/test_python_manager.py ================================================ from __future__ import annotations import os import sys import textwrap from pathlib import Path from typing import TYPE_CHECKING import findpython import packaging.version import pytest from poetry.core.constraints.version import Version from poetry.utils.env.python import Python if TYPE_CHECKING: from unittest.mock import MagicMock from pytest_mock import MockerFixture from poetry.config.config import Config from tests.types import MockedPythonRegister from tests.types import ProjectFactory @pytest.fixture(scope="session") def python_version() -> Version: version = sys.version.split(" ", 1)[0] if version[-1] == "+": version = version[:-1] return Version.parse(version) def test_python_get_version_on_the_fly() -> None: python = Python.get_system_python() assert python.version == Version.parse( ".".join([str(s) for s in sys.version_info[:3]]) ) assert python.patch_version == Version.parse( ".".join([str(s) for s in sys.version_info[:3]]) ) assert python.minor_version == Version.parse( ".".join([str(s) for s in sys.version_info[:2]]) ) def test_python_get_system_python() -> None: python = Python.get_system_python() assert python.executable.resolve() == findpython.find().executable.resolve() assert python.version == Version.parse( ".".join(str(v) for v in sys.version_info[:3]) ) def test_python_get_preferred_default(config: Config, python_version: Version) -> None: python = Python.get_preferred_python(config) assert python.executable.resolve() == Path(sys.executable).resolve() assert python.version == python_version def test_get_preferred_python_use_poetry_python_disabled( config: Config, mocker: MockerFixture ) -> None: mocker.patch( "poetry.utils.env.python.Python.get_active_python", return_value=Python( python=findpython.PythonVersion( executable=Path("/usr/bin/python3.7"), _version=packaging.version.Version("3.7.1"), _interpreter=Path("/usr/bin/python3.7"), ) ), ) config.config["virtualenvs"]["use-poetry-python"] = False python = Python.get_preferred_python(config) assert python.executable.as_posix().startswith("/usr/bin/python") assert python.version == Version.parse("3.7.1") def test_get_preferred_python_use_poetry_python_disabled_fallback( config: Config, with_no_active_python: MagicMock ) -> None: config.config["virtualenvs"]["use-poetry-python"] = False python = Python.get_preferred_python(config) assert with_no_active_python.call_count == 1 assert python.executable.resolve() == Path(sys.executable).resolve() def test_fallback_on_detect_active_python(with_no_active_python: MagicMock) -> None: active_python = Python.get_active_python() assert active_python is None assert with_no_active_python.call_count == 1 @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_detect_active_python_with_bat( tmp_path: Path, without_mocked_findpython: None, python_version: Version ) -> None: """On Windows pyenv uses batch files for python management.""" python_wrapper = tmp_path / "python.bat" with python_wrapper.open("w", encoding="locale") as f: f.write( textwrap.dedent(f""" @echo off SET PYTHON_EXE="{sys.executable}" %PYTHON_EXE% %* """) ) os.environ["PATH"] = str(python_wrapper.parent) + os.pathsep + os.environ["PATH"] python = Python.get_active_python() assert python is not None # TODO: Asses if Poetry needs to discover real path in these cases as # this is not a symlink and won't be handled by findpython assert python.executable.as_posix() == Path(sys.executable).as_posix() assert python.version == python_version def test_python_find_compatible( project_factory: ProjectFactory, mocked_python_register: MockedPythonRegister ) -> None: # Note: This test may fail on Windows systems using Python from the Microsoft Store, # as the executable is named `py.exe`, which is not currently recognized by # Python.get_compatible_python. This issue will be resolved in #2117. # However, this does not cause problems in our case because Poetry's own # Python interpreter is used before attempting to find another compatible version. fixture = Path(__file__).parent.parent / "fixtures" / "simple_project" poetry = project_factory("simple-project", source=fixture) mocked_python_register("3.12") python = Python.get_compatible_python(poetry) assert Version.from_parts(3, 4) <= python.version <= Version.from_parts(4, 0) ================================================ FILE: tests/utils/test_threading.py ================================================ from __future__ import annotations import functools import logging import os import sys import time from concurrent.futures import wait from concurrent.futures.thread import ThreadPoolExecutor from typing import TYPE_CHECKING import pytest from poetry.utils.threading import AtomicCachedProperty from poetry.utils.threading import atomic_cached_property if TYPE_CHECKING: from collections.abc import Generator from pytest import LogCaptureFixture from pytest_mock import MockerFixture WORKER_COUNT = (os.cpu_count() or 1) + 4 EXPECTED_VALUE = sum(range(1_00_000)) IS_PY_312 = (sys.version_info.major, sys.version_info.minor) >= (3, 12) class Example: def __init__(self, value: int = 0, name: str = "default") -> None: self.value = value self._name = name @classmethod def compute_value(cls, name: str, ts: float) -> int: logging.getLogger().info( "Example compute_value called with name=%s time=%f", name, ts ) return sum(range(1_00_000)) def _compute_value(self) -> int: # we block the thread here to ensure contention time.sleep(0.05) return self.compute_value(self._name, time.time()) @functools.cached_property def value_functools_cached_property(self) -> int: return self._compute_value() + self.value @property @functools.cache # noqa: B019 def value_functools_cache(self) -> int: return self._compute_value() + self.value @atomic_cached_property def value_atomic_cached_property(self) -> int: return self._compute_value() + self.value @pytest.fixture(autouse=True) def capture_logging(caplog: LogCaptureFixture) -> Generator[None]: with caplog.at_level(logging.DEBUG): yield def test_threading_property_types() -> None: assert isinstance(Example.value_atomic_cached_property, AtomicCachedProperty) assert isinstance( Example.value_functools_cached_property, functools.cached_property ) assert isinstance(Example.value_functools_cache, property) def test_threading_single_thread_safe() -> None: instance = Example() assert ( instance.value_functools_cached_property == instance.value_atomic_cached_property == EXPECTED_VALUE ) def run_in_threads(instance: Example, property_name: str) -> None: results = [] def access_property() -> None: results.append(instance.__getattribute__(property_name)) executor = ThreadPoolExecutor(max_workers=WORKER_COUNT) futures = [executor.submit(access_property) for _ in range(WORKER_COUNT)] wait(futures) assert len(results) == WORKER_COUNT assert all(result == (EXPECTED_VALUE + instance.value) for result in results) @pytest.mark.parametrize( ["property_name", "expected_call_count"], [ ("value_atomic_cached_property", 1), # prior to Python 3.12, cached_property did have a thread lock ("value_functools_cached_property", WORKER_COUNT if IS_PY_312 else 1), ("value_functools_cache", WORKER_COUNT), ], ) def test_threading_property_caching( property_name: str, expected_call_count: int, mocker: MockerFixture, caplog: LogCaptureFixture, ) -> None: compute_value_spy = mocker.spy(Example, "compute_value") run_in_threads(Example(), property_name) assert compute_value_spy.call_count == len(caplog.messages) == expected_call_count @pytest.mark.parametrize( ["property_name", "expected_call_count"], [ ("value_atomic_cached_property", 2), # prior to Python 3.12, cached_property did have a thread lock ("value_functools_cached_property", (WORKER_COUNT if IS_PY_312 else 1) * 2), ("value_functools_cache", WORKER_COUNT * 2), ], ) def test_threading_atomic_cached_property_different_instances( property_name: str, expected_call_count: int, mocker: MockerFixture, caplog: LogCaptureFixture, ) -> None: compute_value_spy = mocker.spy(Example, "compute_value") instance1 = Example(10, "one") instance2 = Example(20, "two") run_in_threads(instance1, property_name) run_in_threads(instance2, property_name) assert compute_value_spy.call_count == len(caplog.messages) == expected_call_count assert instance1.__getattribute__(property_name) == EXPECTED_VALUE + 10 assert instance2.__getattribute__(property_name) == EXPECTED_VALUE + 20 ================================================ FILE: tests/vcs/git/conftest.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING import dulwich.repo import pytest from tests.vcs.git.git_fixture import TempRepoFixture if TYPE_CHECKING: from pathlib import Path @pytest.fixture() def temp_repo(tmp_path: Path) -> TempRepoFixture: """Temporary repository with 2 commits""" repo = dulwich.repo.Repo.init(str(tmp_path), default_branch=b"main") worktree = repo.get_worktree() # init commit (tmp_path / "foo").write_text("foo", encoding="utf-8") worktree.stage(["foo"]) init_commit = worktree.commit( committer=b"User ", author=b"User ", message=b"init", no_verify=True, sign=False, ) # one commit which is not "head" (tmp_path / "bar").write_text("bar", encoding="utf-8") worktree.stage(["bar"]) middle_commit = worktree.commit( committer=b"User ", author=b"User ", message=b"extra", no_verify=True, sign=False, ) # extra commit (tmp_path / "third").write_text("third file", encoding="utf-8") worktree.stage(["third"]) head_commit = worktree.commit( committer=b"User ", author=b"User ", message=b"extra", no_verify=True, sign=False, ) repo[b"refs/tags/v1"] = head_commit return TempRepoFixture( path=tmp_path, repo=repo, init_commit=init_commit.decode(), middle_commit=middle_commit.decode(), head_commit=head_commit.decode(), ) ================================================ FILE: tests/vcs/git/git_fixture.py ================================================ from __future__ import annotations import typing if typing.TYPE_CHECKING: from pathlib import Path import dulwich.repo class TempRepoFixture(typing.NamedTuple): path: Path repo: dulwich.repo.Repo init_commit: str middle_commit: str head_commit: str ================================================ FILE: tests/vcs/git/test_backend.py ================================================ from __future__ import annotations import shutil from typing import TYPE_CHECKING from typing import cast import pytest from dulwich.client import FetchPackResult from dulwich.refs import HEADREF from dulwich.refs import Ref from dulwich.repo import Repo from poetry.console.exceptions import PoetryRuntimeError from poetry.vcs.git.backend import Git from poetry.vcs.git.backend import GitRefSpec from poetry.vcs.git.backend import is_revision_sha from poetry.vcs.git.backend import peeled_tag from poetry.vcs.git.backend import urlpathjoin from tests.helpers import MOCK_DEFAULT_GIT_REVISION if TYPE_CHECKING: from pathlib import Path from pytest_mock import MockerFixture from tests.vcs.git.git_fixture import TempRepoFixture VALID_SHA = "c5c7624ef64f34d9f50c3b7e8118f7f652fddbbd" FULL_SHA_MAIN = "f7c3bc1d808e04732adf679965ccc34ca7ae3441" FULL_SHA_TAG = "d4f6c2a8b9e1073451f28c96a5db7e3f9c2a8b7e" SHORT_SHA = "f7c3bc1d" @pytest.fixture() def repo_mock(mocker: MockerFixture) -> Repo: repo = mocker.MagicMock(spec=Repo) repo.get_config.return_value.get.return_value = ( b"https://github.com/python-poetry/poetry.git" ) repo.head.return_value = MOCK_DEFAULT_GIT_REVISION.encode("utf-8") # Mock object store for short SHA resolution repo.object_store = mocker.MagicMock() repo.object_store.iter_prefix.return_value = [FULL_SHA_MAIN.encode()] return cast("Repo", repo) @pytest.fixture() def fetch_pack_result(mocker: MockerFixture) -> FetchPackResult: mock_fetch_pack_result = mocker.MagicMock(spec=FetchPackResult) mock_fetch_pack_result.refs = { b"refs/heads/main": FULL_SHA_MAIN.encode(), b"refs/heads/feature": b"a9b8c7d6e5f4321098765432109876543210abcd", b"refs/tags/v1.0.0": FULL_SHA_TAG.encode(), peeled_tag(b"refs/tags/v1.0.0"): FULL_SHA_TAG.encode(), b"HEAD": FULL_SHA_MAIN.encode(), } mock_fetch_pack_result.symrefs = {b"HEAD": b"refs/heads/main"} return cast("FetchPackResult", mock_fetch_pack_result) def test_invalid_revision_sha() -> None: result = is_revision_sha("invalid_input") assert result is False def test_valid_revision_sha() -> None: result = is_revision_sha(VALID_SHA) assert result is True def test_invalid_revision_sha_min_len() -> None: result = is_revision_sha("c5c7") assert result is False def test_invalid_revision_sha_max_len() -> None: result = is_revision_sha(VALID_SHA + "42") assert result is False @pytest.mark.parametrize( ("url"), [ "git@github.com:python-poetry/poetry.git", "https://github.com/python-poetry/poetry.git", "https://github.com/python-poetry/poetry", "https://github.com/python-poetry/poetry/", ], ) def test_get_name_from_source_url(url: str) -> None: name = Git.get_name_from_source_url(url) assert name == "poetry" @pytest.mark.parametrize(("tag"), ["my-tag", b"my-tag"]) def test_peeled_tag(tag: str | bytes) -> None: tag = peeled_tag("my-tag") assert tag == b"my-tag^{}" def test_get_remote_url(repo_mock: Repo) -> None: assert ( Git.get_remote_url(repo_mock) == "https://github.com/python-poetry/poetry.git" ) def test_get_revision(repo_mock: Repo) -> None: assert Git.get_revision(repo_mock) == MOCK_DEFAULT_GIT_REVISION def test_info(repo_mock: Repo) -> None: info = Git.info(repo_mock) assert info.origin == "https://github.com/python-poetry/poetry.git" assert ( info.revision == MOCK_DEFAULT_GIT_REVISION ) # revision already mocked in helper @pytest.mark.parametrize( "url, expected_result", [ ("ssh://git@github.com/org/repo", "ssh://git@github.com/other-repo"), ("ssh://git@github.com/org/repo/", "ssh://git@github.com/org/other-repo"), ], ) def test_urlpathjoin(url: str, expected_result: str) -> None: path = "../other-repo" result = urlpathjoin(url, path) assert result == expected_result def test_git_refspec() -> None: git_ref = GitRefSpec("main", "1234", "v2") assert git_ref.branch == "main" assert git_ref.revision == "1234" assert git_ref.tag == "v2" assert git_ref.ref == b"HEAD" @pytest.mark.parametrize( "refspec, expected_ref, expected_branch, expected_revision, expected_tag", [ # Basic parameter tests ( GitRefSpec(branch="main"), b"refs/heads/main", "main", None, None, ), ( GitRefSpec(tag="v1.0.0"), peeled_tag(b"refs/tags/v1.0.0"), None, None, "v1.0.0", ), ( GitRefSpec(branch="refs/heads/feature"), b"refs/heads/feature", "refs/heads/feature", None, None, ), # Cross-parameter resolution tests ( GitRefSpec(revision="v1.0.0"), peeled_tag(b"refs/tags/v1.0.0"), None, None, "v1.0.0", ), ( GitRefSpec(revision="main"), b"refs/heads/main", "main", None, None, ), ( GitRefSpec(branch="v1.0.0"), peeled_tag(b"refs/tags/v1.0.0"), None, None, "v1.0.0", ), ( GitRefSpec(revision="refs/heads/main"), b"refs/heads/main", "refs/heads/main", None, None, ), # SHA resolution tests with realistic values ( GitRefSpec(revision=SHORT_SHA), b"refs/heads/main", None, FULL_SHA_MAIN, None, ), ( GitRefSpec(revision=FULL_SHA_MAIN), b"refs/heads/main", None, FULL_SHA_MAIN, None, ), ], ) def test_git_ref_spec_resolve( fetch_pack_result: FetchPackResult, repo_mock: Repo, refspec: GitRefSpec, expected_ref: bytes, expected_branch: str | None, expected_revision: str | None, expected_tag: str | None, ) -> None: refspec.resolve(fetch_pack_result, repo_mock) assert refspec.ref == expected_ref assert refspec.branch == expected_branch assert refspec.revision == expected_revision assert refspec.tag == expected_tag @pytest.mark.skip_git_mock def test_clone_success(tmp_path: Path, temp_repo: TempRepoFixture) -> None: source_root_dir = tmp_path / "test-repo" Git.clone( url=temp_repo.path.as_uri(), source_root=source_root_dir, name="clone-test" ) target_dir = source_root_dir / "clone-test" assert (target_dir / ".git").is_dir() @pytest.mark.skip_git_mock def test_short_sha_not_in_head(tmp_path: Path, temp_repo: TempRepoFixture) -> None: source_root_dir = tmp_path / "test-repo" Git.clone( url=temp_repo.path.as_uri(), revision=temp_repo.middle_commit[:6], name="clone-test", source_root=source_root_dir, ) target_dir = source_root_dir / "clone-test" assert (target_dir / ".git").is_dir() @pytest.mark.skip_git_mock def test_clone_existing_locked_tag(tmp_path: Path, temp_repo: TempRepoFixture) -> None: source_root_dir = tmp_path / "test-repo" source_url = temp_repo.path.as_uri() Git.clone(url=source_url, source_root=source_root_dir, name="clone-test") tag_ref = source_root_dir / "clone-test" / ".git" / "refs" / "tags" / "v1" assert tag_ref.is_file() tag_ref_lock = tag_ref.with_name("v1.lock") shutil.copy(tag_ref, tag_ref_lock) with pytest.raises(PoetryRuntimeError) as exc_info: Git.clone(url=source_url, source_root=source_root_dir, name="clone-test") expected_short = ( f"Failed to clone {source_url} at 'refs/heads/main'," f" unable to acquire file lock for {tag_ref}." ) assert str(exc_info.value) == expected_short assert exc_info.value.get_text(debug=True, strip=True) == ( f"{expected_short}\n\n" "Note: This error arises from interacting with the specified vcs source" " and is likely not a Poetry issue.\n" "This issue could be caused by any of the following;\n\n" "- another process is holding the file lock\n" "- another process crashed while holding the file lock\n\n" f"Try again later or remove the {tag_ref_lock} manually" " if you are sure no other process is holding it." ) @pytest.mark.skip_git_mock def test_clone_annotated_tag(tmp_path: Path) -> None: """Test cloning at an annotated tag (issue #10658).""" from dulwich import porcelain from dulwich.objects import Commit # Create a source repository with an annotated tag source_path = tmp_path / "source-repo" source_path.mkdir() repo = Repo.init(str(source_path)) # Create initial commit test_file = source_path / "test.txt" test_file.write_text("test content", encoding="utf-8") porcelain.add(repo, str(test_file)) expected_commit_sha = porcelain.commit( repo, message=b"Initial commit", author=b"Test ", committer=b"Test ", ) # Create an annotated tag porcelain.tag_create( repo, tag=b"v1.0.0", message=b"Release 1.0.0", author=b"Test ", annotated=True, ) # Clone at the annotated tag source_root_dir = tmp_path / "clone-root" source_root_dir.mkdir() cloned_repo = Git.clone( url=source_path.as_uri(), source_root=source_root_dir, name="clone-test", tag="v1.0.0", ) # Verify HEAD points to a commit, not a tag object head_sha = cloned_repo.refs[HEADREF] head_obj = cloned_repo.object_store[head_sha] assert isinstance(head_obj, Commit), ( f"HEAD should point to a Commit, got {type(head_obj).__name__}" ) # Verify it's the correct commit assert head_sha == expected_commit_sha, ( f"HEAD should point to the expected commit {expected_commit_sha.hex()}, " f"got {head_sha.hex()}" ) # Verify the clone succeeded and files are present clone_dir = source_root_dir / "clone-test" assert (clone_dir / ".git").is_dir() assert (clone_dir / "test.txt").exists() assert (clone_dir / "test.txt").read_text(encoding="utf-8") == "test content" @pytest.mark.skip_git_mock def test_clone_nested_annotated_tags(tmp_path: Path) -> None: """Test cloning at a tag that points to another tag (nested tags).""" from dulwich import porcelain from dulwich.objects import Commit from dulwich.objects import Tag # Create a source repository with nested annotated tags source_path = tmp_path / "source-repo" source_path.mkdir() repo = Repo.init(str(source_path)) # Create initial commit test_file = source_path / "test.txt" test_file.write_text("nested tag test", encoding="utf-8") porcelain.add(repo, paths=[b"test.txt"]) commit_sha = porcelain.commit( repo, message=b"Initial commit", committer=b"Test ", author=b"Test ", ) # Create first annotated tag pointing to the commit tag1 = Tag() tag1.name = b"v1.0.0" tag1.object = (Commit, commit_sha) tag1.message = b"First tag" tag1.tag_time = 1234567890 tag1.tag_timezone = 0 tag1.tagger = b"Test " repo.object_store.add_object(tag1) repo.refs[Ref(b"refs/tags/v1.0.0")] = tag1.id # Create second annotated tag pointing to the first tag tag2 = Tag() tag2.name = b"v1.0.0-release" tag2.object = (Tag, tag1.id) tag2.message = b"Second tag (points to first tag)" tag2.tag_time = 1234567891 tag2.tag_timezone = 0 tag2.tagger = b"Test " repo.object_store.add_object(tag2) repo.refs[Ref(b"refs/tags/v1.0.0-release")] = tag2.id # Clone at the nested tag source_root_dir = tmp_path / "clone-root" source_root_dir.mkdir() cloned_repo = Git.clone( url=source_path.as_uri(), source_root=source_root_dir, name="clone-test", tag="v1.0.0-release", ) # Verify HEAD points to a commit, not a tag object head_sha = cloned_repo.refs[HEADREF] head_obj = cloned_repo.object_store[head_sha] assert isinstance(head_obj, Commit), ( f"HEAD should point to a Commit (peeling nested tags), got {type(head_obj).__name__}" ) # Verify it's the correct commit assert head_sha == commit_sha # Verify the clone succeeded and files are present clone_dir = source_root_dir / "clone-test" assert (clone_dir / ".git").is_dir() assert (clone_dir / "test.txt").exists() assert (clone_dir / "test.txt").read_text(encoding="utf-8") == "nested tag test" ================================================ FILE: tests/vcs/git/test_system.py ================================================ from __future__ import annotations import re import shutil import subprocess import typing import pytest from poetry.vcs.git.system import SystemGit if typing.TYPE_CHECKING: from pathlib import Path from tests.vcs.git.git_fixture import TempRepoFixture GIT_NOT_INSTALLED = shutil.which("git") is None def get_head_sha(cwd: Path) -> str: return subprocess.check_output( ["git", "rev-parse", "HEAD"], cwd=cwd, text=True, encoding="utf-8", ).strip() @pytest.mark.skipif(GIT_NOT_INSTALLED, reason="These tests requires git cli") class TestSystemGit: def test_clone_success(self, tmp_path: Path, temp_repo: TempRepoFixture) -> None: target_dir = tmp_path / "test-repo" SystemGit.clone(temp_repo.path.as_uri(), target_dir) assert (target_dir / ".git").is_dir() def test_clone_invalid_parameter(self, tmp_path: Path) -> None: with pytest.raises( RuntimeError, match=re.escape("Invalid Git parameter: --upload-pack") ): SystemGit.clone("--upload-pack=touch ./HELL", tmp_path) def test_checkout_1(self, temp_repo: TempRepoFixture) -> None: # case 1 - with 'target' arg SystemGit.checkout(temp_repo.init_commit[:12], temp_repo.path) assert get_head_sha(temp_repo.path) == temp_repo.init_commit def test_checkout_2( self, monkeypatch: pytest.MonkeyPatch, temp_repo: TempRepoFixture ) -> None: # case 2 - without 'target' arg monkeypatch.chdir(temp_repo.path) SystemGit.checkout(temp_repo.init_commit[:12]) assert get_head_sha(temp_repo.path) == temp_repo.init_commit